Guides
Record Lookup
Full records for companies, people, and documents you already know — batch lookups with misses reported rather than omitted.
Search finds; Record Lookup enriches. When you already know which records you want, the entities routes return the full record for each one, in batch, with every miss reported honestly instead of silently dropped. The identifiers might be:
- a list of domains from your CRM,
- the domains and LinkedIn URLs on a search you just ran,
- LinkedIn URLs from a signup form.
Use Record Lookup when the identifiers are already in hand. If you are still discovering which records exist, that is Search. If you want more companies like ones you have, that is Similar Companies.
Use cases#
Three situations cover most uses of these routes.
Your CRM holds domains; your team needs firmographics (industry, size, funding) on every record. You get one result per domain, in order: fields to write back, and misses to route to a manual queue.
A search gave you lean rows; the next step needs the complete record. You get full records for every resolvable hit: an export-ready dataset in two calls.
You want to know how much of your account list the index can serve. You get a known-or-not answer per account, so the hit rate shows what an enrichment integration will deliver before you build it.
One call, up to 50 domains with by, misses included:
curl -X POST https://api.sproutsdata.ai/v1/entities/companies \
-H "X-API-Key: $SPROUTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ids": ["adyen.com", "checkout.com"],
"by": "domain",
"fields": ["account_name", "industry", "employee_count"]
}'Outcome: one result per domain you sent, in order. found: true rows carry the requested fields under matches; found: false rows mark what the index doesn't hold. Write the fields back to your CRM and route the misses to a manual queue.
How it works#
The entities routes return full records by id. There is one route per category (nine paths with plural slugs) rather than a category parameter in the body. The reference has a page for companies, persons and the seven document routes:
POST /v1/entities/companies POST /v1/entities/market-reports
POST /v1/entities/persons POST /v1/entities/fundings
POST /v1/entities/patents POST /v1/entities/academic-reports
POST /v1/entities/news POST /v1/entities/job-openings
POST /v1/entities/trends
All nine require the dsp.entities.read scope and are metered under the dsp.entities action.
Request
| Field | Notes |
|---|---|
ids | Required, 1 to the route's cap (below). Order and duplicates preserved in the response |
fields | Optional list of field names. Omit for the full record |
by | How to read ids. companies/persons resolve raw identifiers; four document routes choose between id and name (below) |
Batch caps
The ceilings differ by an order of magnitude. The reason is response size, not caution: a company record is a few kilobytes; a trend document averages 4.5 MB.
| Route | Max ids per call |
|---|---|
companies, persons | 1,000 — 50 when by is set |
news, market-reports, fundings, job-openings | 100 with by: "id" — 50 with by: "name", which is the default |
patents, trends, academic-reports | 100 |
The by cap is tighter on companies and persons because each raw value has to be classified before it can be looked up, and on the four named document routes because one name can resolve to several documents. Exceeding a cap is a 422 before anything is charged.
by — resolving raw identifiers
What by accepts depends on the route.
Without by, ids are internal record ids. The id on a search hit is not one — it is the company's domain or an opaque 16-hex hash — so look a hit up by its own fields: a company hit with by: "domain" and its domain, a person hit with by: "li_url" and its linkedin_url. With by, ids are raw values:
by | Meaning | Available on |
|---|---|---|
auto | Classify each value independently (the same detection /v1/similar uses on anchors) | companies, persons |
domain | Every value is a company domain | companies only — a person has no domain |
li_url | Every value is a LinkedIn URL | companies, persons |
name | Every value is a name (full-text match, least reliable). Returns only the best-ranked match, so matches holds one entry | companies, persons |
Response
The standard envelope, where each result is an EntityHit:
{
"results": [
{
"id": "adyen.com",
"category": "company",
"found": true,
"matches": [{ "account_name": "Adyen", "industry": "Finance", "employee_count": 4000 }]
},
{
"id": "definitely-not-a-real-company.example",
"category": "company",
"found": false,
"matches": []
}
],
"total": 2,
"total_is_estimate": false,
"has_more": false,
"next_cursor": null,
"resolved_search": null,
"degradation": [],
"meta": { "request_id": "bd22794d0c5c06f1d3e7045e3c0e32ab", "environment": "live", "as_of": "2026-08-13T10:00:00Z", "credential": "api_key" }
}Each hit:
| Field | Meaning |
|---|---|
id | The id you asked with, echoed back |
category | Always the route's category |
found | Whether the record exists. Misses come back as found: false; they are never omitted |
matches | The records, always a list, and [] on a miss. Typed for companies and persons; the document categories return each vertical's own field vocabulary |
An id lookup resolves to exactly one entry, and so does a company or person lookup by any by, so matches[0] is the record.
Record Lookup responses are never paginated: has_more is always false, next_cursor always null, and total is the exact count of hits returned (found and not-found together).
Chaining from search
Search hits chain into record lookups for companies and persons. A hit's id is not a lookup id, so send one of the hit's own fields instead:
- A
companyhit chains viaPOST /v1/entities/companieswithby: "domain"and the hit'sdomain. - A
personhit chains viaPOST /v1/entities/personswithby: "li_url"and the hit'slinkedin_url. - A hit with neither field can't be chained. When a company has no domain its
idis a 16-hex hash, which is one-way, and the entity routes don't accept it.
import os, requests
BASE = "https://api.sproutsdata.ai"
headers = {"X-API-Key": os.environ["SPROUTS_API_KEY"]}
# Companies: look each hit up by its domain.
companies = requests.post(f"{BASE}/v1/search", headers=headers, json={
"query": "payment orchestration platforms", "category": "company", "limit": 10,
}).json()
domains = [h["domain"] for h in companies["results"] if h.get("domain")][:50]
records = requests.post(f"{BASE}/v1/entities/companies", headers=headers, json={
"ids": domains, "by": "domain",
"fields": ["account_name", "industry", "employee_count", "total_funding"],
}).json()
for hit in records["results"]:
if hit["found"]:
record = hit["matches"][0]
print(hit["id"], record.get("account_name"))
# People: look each hit up by its LinkedIn URL.
people = requests.post(f"{BASE}/v1/search", headers=headers, json={
"category": "person",
"filters": {"title": {"include": ["head of payments"]}},
"limit": 10,
}).json()
urls = [h["linkedin_url"] for h in people["results"] if h.get("linkedin_url")][:50]
records = requests.post(f"{BASE}/v1/entities/persons", headers=headers, json={
"ids": urls, "by": "li_url",
"fields": ["contact_name", "title", "employment_history"],
}).json()Errors
The entities routes share one error surface:
| Status | When |
|---|---|
| 401 / 402 / 403 | Charged and scoped |
| 422 | Validation, including the batch caps |
| 429 | Reserved for rate limits. There is no per-key rate limit today, but if you get a 429, wait for Retry-After and retry |
| 503 / 504 | The retrieval engine is down (not charged) |
There is no 501 here. An unwired vertical 501s on search; a lookup by id either finds the record or reports the miss.
Best practices#
- Know the batch caps before you batch. 1,000 internal ids for companies/persons, 50 when
byis set; 100 on document routes, 50 for aby: "name"lookup. Exceeding a cap is a 422 before anything is charged. - Treat
found: falseas data, not noise. Misses are free and reported, so a list offalserows is a coverage report over your own data. - Request only the
fieldsyou need. Full records are the default and can be large (a trend document averages 4.5 MB); a field list keeps batches fast and cheap to move. - Read
matchesas a list.matches[0]is the record for an id lookup; aby: "name"lookup on the four named document routes can return several, and taking the first is a silent choice you did not make. - Read
attribution[].as_ofwhen freshness matters. It is the per-corpus answer to "how current is this employee count?", stated once for the response rather than per field.