# Bulk jobs: poll or webhook The sync [People Bulk Enrich](https://getprospect.com/api-docs/people-bulk-enrich.md) and [Company Bulk Enrich](https://getprospect.com/api-docs/company-bulk-enrich.md) answer up to 50 items in the request. For bigger lists, send them as one **job** of up to **10,000 rows**. It runs the same pipeline as the CSV enrichment in the app: a database match, the stored email reveal, then the live email finder for rows with a name plus a company or domain. Bulk email verification works the same way. | Job | Create | Read | Results | | --- | --- | --- | --- | | People enrichment | [Create People Enrichment](https://getprospect.com/api-docs/people-enrichments-create.md) | [Get Enrichment](https://getprospect.com/api-docs/enrichments-get.md) | [Get Enrichment Results](https://getprospect.com/api-docs/enrichments-results.md) | | Company enrichment | [Create Company Enrichment](https://getprospect.com/api-docs/companies-enrichments-create.md) | [Get Enrichment](https://getprospect.com/api-docs/enrichments-get.md) | [Get Enrichment Results](https://getprospect.com/api-docs/enrichments-results.md) | | Email verification | [Create Email Verification](https://getprospect.com/api-docs/email-verifications-create.md) | [Get Email Verification](https://getprospect.com/api-docs/email-verifications-get.md) | [Get Email Verification Results](https://getprospect.com/api-docs/email-verifications-results.md) | ## 1. Create the job The rows are the items of the sync bulk enrich, each with an optional `identifier` of yours that comes back on its result. The answer is `202` at once, with the job and `Location: /v2/jobs/{id}`. There is no cost estimate and no start call: the job is already running. ```bash curl --request POST \ --url https://api.getprospect.com/v2/people/enrichments \ --header 'Content-Type: application/json' \ --header 'x-api-key: YOUR_API_KEY' \ --header 'Idempotency-Key: crm-sync-2026-10-18' \ --data ' { "name": "Webinar leads", "data": [ { "identifier": "crm-1", "linkedin_url": "https://www.linkedin.com/in/jane-doe" }, { "identifier": "crm-2", "first_name": "John", "last_name": "Smith", "company_domain": "acme.com" }, { "identifier": "crm-3", "email": "ann@globex.com" } ], "enrich_email": true, "enrich_phone": false }' ``` Send an `Idempotency-Key` and retry with the same key on a network error: the retry answers the job it created (`Idempotent-Replayed: true`) instead of starting a second one. A row with no usable lookup (a LinkedIn URL, an email, or first and last name with a company domain or name) does not fail the request; it comes back as `invalid`. ## 2. Wait for it **Poll** [Get Job](https://getprospect.com/api-docs/jobs-get.md) (or the job's own resource) until `status` is `done`, `failed` or `cancelled`. While it runs, the answer carries `Retry-After` in seconds: wait that long between polls. Polls are free and do not count against the per-minute rate limit. **Or subscribe once** with [Create Webhook](https://getprospect.com/api-docs/webhooks-create.md) and `"events": ["job.finished"]`. Every enrichment and email verification of your workspace then sends one signed `job.finished` event when it ends, with the job and its links in `data`. The event carries no person data: read the results with your key. ```json { "id": "evt_6702a1b2c3d4e5f607182931", "type": "job.finished", "api_version": "2026-10", "created_at": "2026-10-18T12:01:10.000Z", "webhook_id": "6702a1b2c3d4e5f607182930", "data": { "job": { "id": "6702a1b2c3d4e5f607182901", "type": "csv_enrichment", "status": "done", "...": "..." }, "links": { "self": "/v2/enrichments/6702a1b2c3d4e5f607182901", "job": "/v2/jobs/6702a1b2c3d4e5f607182901", "results": "/v2/enrichments/6702a1b2c3d4e5f607182901/results" } } } ``` Jobs created through the API send no notifications in the app (no bell, no "file ready" email): the job resource and `job.finished` are the only signals. ## 3. Read the results Page [Get Enrichment Results](https://getprospect.com/api-docs/enrichments-results.md) with `limit` (up to 500) and send `metadata.pagination.next_cursor` back as `cursor` until it is `null`. There is one item per request row, in request order, each with your `identifier` and a `status`: - **`matched`** - found; `person` or `company` is the same object the sync bulk enrich returns, and `matched_by` says how. `matched_by: "email_finder"` means the live finder worked out the email from the name and company; that person carries what the row said plus the email. - **`not_matched`** - nothing found. - **`invalid`** - the row had no usable lookup; `error.message` says what to send. - **`failed`** - a lookup errored on our side; send the row again. - **`not_processed`** - the job ran out of credits before reaching the row; send it again after a top-up. Only `valid` emails are returned. To get the whole result in one download, the job's `links.jsonl` (one result item per line) and `links.csv` (your rows with the enrichment columns, as the app exports it) are there once it completed. Results are kept for 30 days (`results_expires_at`); after that the results route answers `410`. ```js const BASE = "https://api.getprospect.com"; const headers = { "x-api-key": process.env.GETPROSPECT_API_KEY, "content-type": "application/json" }; const created = await fetch(`${BASE}/v2/people/enrichments`, { method: "POST", headers: { ...headers, "Idempotency-Key": "crm-sync-2026-10-18" }, body: JSON.stringify({ data: rows }), }).then((r) => r.json()); const id = created.data.id; let job; do { const res = await fetch(`${BASE}/v2/jobs/${id}`, { headers }); job = (await res.json()).data; if (!["done", "failed", "cancelled"].includes(job.status)) { await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("retry-after") || 2))); } } while (!["done", "failed", "cancelled"].includes(job.status)); const results = []; let cursor = null; do { const q = new URLSearchParams({ limit: "500", ...(cursor && { cursor }) }); const page = await fetch(`${BASE}/v2/enrichments/${id}/results?${q}`, { headers }).then((r) => r.json()); results.push(...page.data); cursor = page.metadata.pagination.next_cursor; } while (cursor); ``` ## Credits and limits - **People enrichment:** 1 email credit per person whose email is revealed and your workspace does not already own (the live finder charges only when it finds a valid email), 1 phone credit per mobile number found with `enrich_phone`. Re-enriching people you already revealed is free. Creating the job needs at least one email credit (and one phone credit with `enrich_phone`); the job charges as it runs and stops early, keeping what it found, if the balance runs out. - **Company enrichment:** free. - **Email verification:** 1 email verification credit per unique valid-syntax address, charged when the job starts; the unverified remainder is refunded on cancel or failure. With a short balance the job waits and starts once the credits are there. - **Limits:** 10,000 rows per job; at most 5 active enrichments per workspace (a sixth answers `409 limit_reached`). Creating a job spends 1 unit of the `jobs` rate limit (2 per second, 30 per minute, 1,000 per day): on `429`, wait `Retry-After` and retry. See [Credits and rate limits](https://getprospect.com/api-docs/credits-and-limits.md). ## Recipe: bulk email finder There is no separate bulk finder: a people enrichment with `enrich_email` is one. Send first name, last name and the company domain (or name), then read `person.email` from the results. ```json { "data": [ { "identifier": "lead-1", "first_name": "Sundar", "last_name": "Pichai", "company_domain": "google.com" }, { "identifier": "lead-2", "first_name": "Patrick", "last_name": "Collison", "company_name": "Stripe" } ] } ``` Rows we hold come back `matched` with their stored valid email; for the others the live finder works out the address (`matched_by: "email_finder"`). You pay only for emails found.