Crunchbase Company Lookup by Domain: Resolve Multiple Matches Before CRM Enrichment
“One website, one company” is a dangerous default for CRM enrichment. The supplied stripe.com lookup returned four Crunchbase organizations. Taking items[0] would hide an identity decision inside array order, then let that decision overwrite company records.
A Crunchbase company lookup by domain API should start a reviewable mapping, not an automatic merge. The useful output is a confirmed company identifier with evidence explaining why it belongs to this CRM account.
stripe.com returned four organizations, not one company
The observed company-by-domain response contained domain="stripe.com", total_count=4 and four items:
Candidate id | What the integration should do |
|---|---|
stripe | Inspect its identity evidence alongside the CRM account |
stripe-capital | Retain as a separate candidate pending review |
stripe-841c | Retain as a separate candidate pending review |
stripe-atlas | Retain as a separate candidate pending review |
These are observed identifiers, not an inferred corporate hierarchy. Their spelling does not establish which records are subsidiaries, products or legal entities. The example establishes that a domain can be non-unique; it does not establish why every candidate matched or how every URL is normalized.
Non-unique domains are also discussed in Crunchbase's API FAQs. That is useful identity-management context, not permission to import another service's request filters or response contract. For this project, discovery uses the required domain parameter; no result-limit or pagination input is documented for this operation.
Keep the domain, company ID and UUID in separate columns
Do not replace the CRM's website field with a Crunchbase identifier. Keep a small application-owned mapping record:
| Local field | Purpose |
|---|---|
input_domain | Original lookup input, here stripe.com |
candidate_company_ids | All four returned candidate IDs |
selected_company_id | Confirmed ID, unset while unresolved |
selected_uuid | UUID copied from the confirmed candidate when available |
decision_status | Your application's needs_review or confirmed state |
reviewer_context | Evidence and the CRM account the reviewer intended to match |
retrieved_at | UTC time your application retrieved the candidates |
These names describe your database, not additional API response fields. Preserve the candidate set even after selection so later reviewers can reconstruct the decision.
For the next request, use the selected company ID rather than the website domain. Company details documents organization permalinks, UUIDs and organization profile URLs as inputs; the supplied live checks verified permalink requests. Keeping the UUID separately supports cross-checks without confusing it with the domain or assuming other entity types accept the same identifier.
Require website, founders and business context before accepting a match
The primary Stripe candidate in the supplied evidence had the expected website, https://stripe.com, founders John and Patrick Collison, and a matching business description. Those attributes together support a better decision than either a familiar name or a shared domain alone.
Compare them with the account your CRM actually represents. Is it the payments company, a particular business unit, or a different organization using related branding? Read the available description and relationships before deciding. Do not infer legal ownership from a shared website, and leave the mapping unresolved when the evidence does not distinguish candidates.
Write the reasoning into reviewer_context. An explicit decision such as matching the payments-company account against its website, founders and business context is auditable; “first result” is not. Missing founders or descriptions should lead to more review, not an invented confidence score or a silent default.
APIs for similar apps can be requested through the contact page, and private source-code deployment is available.
Request /companies/details only for the confirmed company ID
The catalog path /companies/details is exposed through the platform operation company-details. Its company parameter identifies an organization; a bare website domain is not a documented detail input.
Use an authorized platform key from the dashboard. The key travels in the query string, so run requests server-side and redact URLs from logs, tickets and exception reports. Opening this guide or copying code sends no data request; deliberately running it does.
This Python example uses requests and a local reviewed-company-map.json following the fields above. Create that file only after reviewing a lookup, copy the selected candidate's UUID when available, and set decision_status to confirmed. The code refreshes candidates and refuses an unconfirmed or changed mapping before requesting details.
import json
import os
from pathlib import Path
import requests
BASE = "https://api.opendata-api.com/api/project/crunchbase"
KEY = os.environ["OPENDATA_API_KEY"]
def read(operation, **params):
try:
response = requests.get(
f"{BASE}/{operation}",
params={"key": KEY, **params}, timeout=30,
)
except requests.RequestException:
raise RuntimeError("Request failed; inspect redacted diagnostics") from None
if not response.ok:
raise RuntimeError(f"HTTP {response.status_code}; no CRM update")
envelope = response.json()
if envelope.get("code") != 200:
raise RuntimeError("Gateway business failure; no CRM update")
return envelope["data"]
lookup = read("company-by-domain", domain="stripe.com")
decision = json.loads(Path("reviewed-company-map.json").read_text())
selected = decision.get("selected_company_id")
candidates = {item["id"]: item for item in lookup["items"]}
if (decision.get("decision_status") != "confirmed"
or decision.get("input_domain") != lookup["domain"]
or selected not in candidates):
raise RuntimeError("Company mapping requires review")
expected_uuid = decision.get("selected_uuid")
if expected_uuid and candidates[selected].get("uuid") != expected_uuid:
raise RuntimeError("Candidate UUID changed; review again")
profile = read("company-details", company=selected)
if (profile.get("id") != selected
or expected_uuid and profile.get("uuid") != expected_uuid):
raise RuntimeError("Detail identity mismatch; no CRM update")
print({"id": profile["id"], "website": profile.get("website")})
HTTP success and gateway business success are separate checks: inspect code, message and data rather than accepting HTTP 200 alone. After business success, the observed company detail payload is a flat company object, not an items array or an extra company wrapper. Apply only reviewed fields to the CRM; this snippet deliberately performs no CRM write.
Give ambiguous matches, empty searches and detail 404s different outcomes
Use different application states for different evidence:
- Ambiguous identity: several domain candidates exist, but none is confirmed. Keep the CRM account unchanged and request review.
- Empty discovery: the observed company-name search could return HTTP 200 with
count=0,items=[]andtotal_pages=0. That is a valid empty search, not a missing-detail error. - Missing detail: a deliberately nonexistent company permalink returned HTTP 404. Preserve the rejected identifier for investigation rather than selecting another candidate automatically.
- Request failure: transport or gateway business failure leaves identity undetermined; do not relabel it as “company absent.”
No no-match response shape was established for domain lookup itself. Do not assume it behaves like the empty company-name search.
If the domain is insufficient, search-companies documents optional category, location, funding_stage and investor filters alongside a name-prefix query. Only name-prefix and empty-result searches were live-probed. The positive search counted 82 matches, returned 15 items and reported six pages. Preserve selectors and sort when using its page input, inspect returned pagination, and do not treat page one as complete. Pages 1–100 are documented; founded, last_funding and name sorts are single-page modes.
Identity checks do not fill null funding fields
A correct organization match does not make its financial data complete. The observed Stripe funding response reported count=24 and summary.rounds_count=24 but contained 15 items. Round amounts, valuations and announcement dates were null. Preserve unknowns as null, not zero or dates inferred from round names. Default announced sorting exposes one page; deeper rank paging is documented, not live-verified here.
For subsequent relationship research, company investors identifies backers, company investments concerns an investor organization's portfolio, and investment search into a recipient company offers lead_only. A search match or historical investment does not prove current investor activity; observed investor results included missing type and stage values. Acquisition, IPO and company-news operations offer additional research paths, not certified ownership, complete transaction history or current affiliations.
This platform project is distinct from the official Crunchbase API: use its documented parameters and platform key, not another service's authentication, filters or cursors. A search for a “free Crunchbase API” does not establish a free allowance; check current project pricing and account terms for access and limits. The defensible CRM result remains a reviewed identity mapping with explicit unknowns—not a guarantee of legal identity, financial completeness or real-time coverage.