Get results
Poll an enrichment request until it's final, match items to your records, and read the person and company result schemas.
Read an enrichment request with the enrichRequestId you got when you submitted it. The response tells you the request's overall status and, for each item, its own status and result.
GET https://app.knock-ai.com/v1/enrichment-requests/{enrichRequestId}Reading a request never starts new enrichment, so polling costs nothing beyond the items you submitted (see Credits and side effects). Each read counts toward your rate limit.
Read a request
curl https://app.knock-ai.com/v1/enrichment-requests/7d0f4f1e-2c3b-4a8e-9a51-0e6f3b8c2d14 \
-H "x-api-key: $KNOCK_API_KEY"{
"enrichRequestId": "7d0f4f1e-2c3b-4a8e-9a51-0e6f3b8c2d14",
"type": "people",
"status": "in_progress",
"createdAt": "2026-10-20T09:15:02.418Z",
"items": [
{
"input": "jane@acme.com",
"status": "completed",
"result": {
"firstName": "Jane",
"lastName": "Doe",
"title": "VP Sales",
"seniority": "VP",
"profession": "Sales",
"linkedinUrl": "https://linkedin.com/in/janedoe",
"location": "Austin, Texas, United States",
"photoUrl": null,
"currentPositionStartDate": "2024-03",
"company": null
}
},
{ "input": "bob@globex.com", "status": "pending", "result": null },
{ "input": "nobody@initech.com", "status": "not_found", "result": null }
]
}Response fields
| Field | Type | Description |
|---|---|---|
enrichRequestId | string (UUID) | The ID of the enrichment request. |
type | people or companies | Which kind of items the request holds. It decides the shape of each result. |
status | string | pending, in_progress, completed, partially_completed or failed. Derived from the items every time you read the request; see Request statuses. |
createdAt | string (ISO 8601, UTC) | When the request was created. The 24-hour deadline counts from this time. |
items | object[] | One entry per distinct input (1 to 100), in the order you submitted them. |
items[].input | string | People: the email, trimmed and lowercased. Companies: the domain or URL exactly as you sent it. |
items[].status | string | pending, completed, not_found or failed. The last three are final; see Item statuses. |
items[].result | object or null | A person or company when the item is completed; null for every other status. |
Every key is always present, including the null ones. A completed item's result can still be null in the rare case that Knock finished the item but holds no data for it, so check result before using it.
Match items to your records
Items come back in the order you submitted them, but repeated inputs are collapsed into one item, so positions can shift. Match on input instead:
- People:
inputis the email trimmed and lowercased. Normalize your own emails the same way before looking them up:email.trim().toLowerCase(). - Companies:
inputis the exact string you sent. For repeated companies it's the first occurrence; later duplicates (for exampleacme.comafterhttps://www.acme.com) map to that same item.
const byEmail = new Map(request.items.map((item) => [item.input, item]));
for (const lead of leads) {
const item = byEmail.get(lead.email.trim().toLowerCase());
if (item?.status === 'completed' && item.result) {
saveEnrichment(lead.id, item.result);
}
}Poll until the request is final
A request is final once its status is completed, partially_completed or failed. Until then, read it again with a growing delay:
- Start short. Some items are final within seconds, so make the first read after about 5 seconds.
- Back off. Double the delay after every read that isn't final, up to a cap of about 2 minutes.
- Honor
429. If you're rate limited, wait the number of seconds in theRetry-Afterheader before the next read. - Retry temporary errors.
5xxresponses (including502 external-api.enrichment.unavailable) and brief401s are never final answers. Back off and read again. - Stop at the deadline. An item still pending 24 hours after
createdAtis settled on the first read after that point (or a later one, if Knock can't check it then). If a request is somehow still not final an hour after the deadline, stop polling and handle the remainingpendingitems as failed, or alert someone.
const BASE_URL = 'https://app.knock-ai.com';
const FINAL_STATUSES = new Set(['completed', 'partially_completed', 'failed']);
const DEADLINE_MS = 24 * 60 * 60 * 1000; // items still pending after 24 h settle on the next read
const GRACE_MS = 60 * 60 * 1000; // keep reading for 1 h after the deadline
const FIRST_DELAY_MS = 5_000;
const MAX_DELAY_MS = 2 * 60_000;
const MAX_AUTH_RETRIES = 5;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/** Reads the request until it's final. Returns the last response either way. */
export async function waitForEnrichment(enrichRequestId) {
let delay = FIRST_DELAY_MS;
let authFailures = 0;
const backOff = () => {
delay = Math.min(delay * 2, MAX_DELAY_MS);
};
for (;;) {
await sleep(delay);
let res;
try {
res = await fetch(`${BASE_URL}/v1/enrichment-requests/${enrichRequestId}`, {
headers: { 'x-api-key': process.env.KNOCK_API_KEY },
signal: AbortSignal.timeout(30_000),
});
} catch {
backOff(); // network error or timeout
continue;
}
if (res.status === 429) {
delay = Number(res.headers.get('retry-after') ?? 60) * 1000;
continue;
}
if (res.status === 401 && authFailures < MAX_AUTH_RETRIES) {
authFailures += 1; // Knock fails closed: a brief 401 can be temporary
backOff();
continue;
}
if (res.status >= 500) {
backOff(); // temporary, never a final answer
continue;
}
if (!res.ok) {
// 400, 403, 404 or a persistent 401: reading again won't help.
const error = await res.json().catch(() => ({}));
throw new Error(`Reading ${enrichRequestId} failed: ${res.status} ${error.code ?? ''}`);
}
authFailures = 0;
const request = await res.json();
if (FINAL_STATUSES.has(request.status)) return request;
const age = Date.now() - Date.parse(request.createdAt);
if (age > DEADLINE_MS + GRACE_MS) {
// Still not final well past the deadline: give up and treat pending items as failed.
return request;
}
backOff();
}
}Poll from a durable job
A request can take up to 24 hours, which is longer than most processes live. In production, store each enrichRequestId with its createdAt and poll from a scheduled job or queue worker, so a restart doesn't lose track of requests in flight.
Budget your reads. All endpoints share one rate-limit bucket per account (600 requests per minute by default). At a 2-minute cap, each request in flight costs about one read every 2 minutes, so hundreds of requests can be polled at once with plenty of room for new submissions. If you have more in flight, raise the cap or poll requests in rotation.
Person result
result for a completed item in a people request. Every key is always present.
| Field | Type | Description |
|---|---|---|
firstName | string or null | First name. |
lastName | string or null | Last name. |
title | string or null | Current job title, for example VP Sales. |
seniority | string or null | Seniority level, for example VP. |
profession | string or null | Professional area, for example Sales. |
linkedinUrl | string | LinkedIn profile URL. Always set: Knock only returns a person once they're matched to a LinkedIn profile. |
location | string or null | Where the person is based, for example Austin, Texas, United States. |
photoUrl | string or null | Profile photo URL. |
currentPositionStartDate | string or null | When the person started their current role. Precision varies, for example 2024-03 or 2024-03-01. |
company | Company or null | The person's current company, with every field of a company result. null while the company isn't resolved yet. |
Company result
result for a completed item in a companies request, and the company of a person result. Every key is always present: text fields Knock has no data for are null, and list fields are empty arrays. Where a text field usually holds one of a known set of values, the typical values are listed, but treat them as free-form strings.
| Field | Type | Description |
|---|---|---|
name | string or null | Company name. |
domain | string or null | Primary domain, for example acme.com. |
logoUrl | string or null | Logo URL. |
description | string or null | Short description of what the company does. |
industry | string or null | Industry in upper snake case, for example COMPUTER_SOFTWARE. |
size | string or null | Employee range. Typically 0-10, 11-50, 51-100, 101-200, 201-300, 301-400, 401-500, 501-600, 601-700, 701-800, 801-900, 901-1000, 1001-5000, 5001-10000 or 10000+. |
employees | string or null | Employee count, as a string. For example 120. |
foundedDate | string or null | When the company was founded, for example 2015. |
totalFunding | string or null | Total funding raised, for example $22M. |
lastFunding | string or null | Type of the latest funding round, for example seed, series_a, private_equity or undisclosed. |
annualRevenue | string or null | Revenue range. Typically $0-$1m, $1m-$10m, $10m-$25m, $25m-$50m, $50m-$100m, $100m-$250m, $250m-$500m, $500m-$1b, $1b-$10b or $10b+. |
ownership | string or null | Ownership type. Typically Private, Public, Acquired/Merged, PARTNERSHIP, NON-PROFIT, government or Other. |
headquarters | string or null | Headquarters location. |
country | string or null | Country of the headquarters: a country name or a two-letter ISO 3166-1 code, depending on the source (for example US). |
region | string or null | Sales region. Typically North America, South America, EMEA, APAC, Japan or Greater China. |
state | string or null | State or province, for example California. |
competitors | string or null | Top competitors, as free text. |
techStack | object[] | Technologies the company uses. |
techStack[].name | string | Technology name, for example Salesforce. |
techStack[].category | string | Technology category, for example CRM. |
teamSize | object[] | Headcount by department. |
teamSize[].department | string | Department name, for example Sales. |
teamSize[].headcount | number | People in that department. |
fundingRounds | object[] | Funding history. |
fundingRounds[].type | string | Round type, for example series_a. |
fundingRounds[].amountMillions | number or null | Amount raised, in millions of currency. |
fundingRounds[].currency | string | Currency of the amount, for example USD. |
fundingRounds[].date | string or null | When the round closed. |
fundingRounds[].investors | string[] | Investor names. |
accountIntel | object or null | How the company goes to market. null when unknown. |
accountIntel.targetPersona | string[] | Who the company sells to. |
accountIntel.segments | string[] | Customer segments it serves. |
accountIntel.verticals | string[] | Customer verticals it serves. |
accountIntel.spaces | string[] | Market spaces it competes in. |
accountIntel.productTypes | string[] | Kinds of products it sells. |
accountIntel.salesMotions | string[] | How it sells. |
decisionMakers | object[] | People at the company who match the decision-maker roles set up in your Knock account. |
decisionMakers[].name | string | Full name. |
decisionMakers[].title | string | Current job title. |
decisionMakers[].linkedinUrl | string | LinkedIn profile URL. |
decisionMakers[].matchedRole | string | The decision-maker role from your Knock account that this person matched. |
New fields may be added to these objects over time, so ignore fields you don't recognize (see Versioning).
Errors
| Status | code | Cause | Retry? |
|---|---|---|---|
400 | validation | enrichRequestId isn't a UUID: enrichRequestId: enrichRequestId must be a UUID. | No |
404 | external-api.enrichment-request.not-found | No request with this ID exists for your account. Requests created with another account's key return 404 too. | No |
502 | external-api.enrichment.unavailable | Results can't be read right now. This is not a final answer about your items. | Yes, with backoff |
Authentication, permission and rate-limit errors apply too. See Errors and the API reference.
Keep your results
Save each item's result on your side as soon as the item is final. Knock may remove old enrichment requests after a retention period, and there's no way to list past requests.