Skip to content

MaintenanceThe former company search endpoint goes into maintenance on November 9Migration guide

Endpoints // Bulk

Bulk enrichment

New

Last verified

Available inAPIMCPSDKCLI

Enrich up to 25,000 people in one request with profiles, emails or phone numbers, priced like the unit calls and refunded for what is not found, then read the results as they come or download them as CSV or NDJSON.

POST/v2/enrich/persons/bulk

Credits

Free to send.

Each item costs what its unit call costs, reserved when the job is created and refunded for what is not found.

Introduction

The Bulk enrichment endpoint enriches up to 25,000 people in one request. Send them as JSON items (a Social profile URL, an email, or a name with its company), choose what to look up for each one (the profile, an email, a phone number), and it runs in the background as a job. It answers at once with the job: follow it with Get a job, or be told on your webhookUrl when it settles. Read its results a page at a time while it runs with Read job results by page, or download one row per item, as CSV or NDJSON, with Download job results.

Bulk enrichment fits a one-off need, such as a CRM export or a file of leads to complete: you get a file back and nothing is kept. To keep people over time, add data to them later and never pay twice for the same person, use a List instead. To look up one person while a user waits, call Enrich profile or Find Email.

Credits

Sending the request is free. Each item costs what its unit call costs, and only for what is found:

What the item looks upCredits
Profile, from a Social profile URL1, or 2 with live: true, as Person profile
Profile, from an email or a name2, as Enrich profile
contact_email3 for a professional email, 5 for a personal one, 8 for both with emailType: "any", as Find Email
contact_phone20 per phone number found, as Find Phone

When the job is created, it reserves the most each item can cost, in the order you sent them, as long as your balance covers it. An item the balance no longer covers is not run: it reads insufficient_credits in the results, costs nothing, and notRunForCredits counts it, while a later item that costs less can still run. When the job settles, what an item did not find is given back: you pay only for what was found. On a Go, Plus or Max plan, a profile your plan covers costs no credit in a job, as it does in its unit call, and once the workspace has delivered its daily profile cap, the items of the job wait until 00:00 UTC instead of being refused.

The email and phone finders start from a Social profile URL, or from a first name, a last name and a company domain. For any other item, the job first matches the person's profile, charged like a profile lookup when it is found: an item with only an email, sent for a phone number, costs 2 credits for its profile, plus 20 if a number is found. An item with only an email is never sent to the email finder, since it already holds one.

An item that names the same person as an earlier item of the job is looked up once: its row reads duplicate, costs nothing, and duplicateCount counts it.

The API key needs the permission of what the job asks for: Person Enrichment when families includes profile (the default), Contact for contact_email and contact_phone. A key with credit limits must hold the whole reservation: a job that would take it over its daily, monthly or total limit is refused with KEY_CREDIT_LIMIT_REACHED before anything is reserved. Once the job settles, the key counts only what it spent.

Creating a job counts as one request on your rate limits, whatever its number of items. Its items then take their share of your per-minute limit as they run, at most 80% of it, and wait when that share is taken: see Rate limits. Reading the job is never counted. Following it, downloading its results and cancelling it still work at a zero balance.

Authorization

NameTypeRequiredDescription
apikeystringYesYour API key from the developer dashboard. Pass it via Authorization: Bearer YOUR_API_KEY header.

Request body

The route is POST /v2/enrich/persons/bulk.

NameTypeRequiredDescription
itemsarrayYes1 to 25,000 people, see Items
familiesstringNoWhat to look up for each item: profile, contact_email, contact_phone. Defaults to ["profile"].
emailTypestringNoWith contact_email, the kind of email to find: professional (default), personal or any
livebooleanNotrue fetches the profile of a Social profile URL item live. Omitted or false, saved data is used.
namestringNoA name for the job, up to 255 characters, shown in the job history and in the app. Defaults to Sent via API.
webhookUrlstringNoPublic HTTPS URL that receives one POST when the job settles, see Webhook callback

Items

NameTypeRequiredDescription
linkedinUrlstringConditionalSocial profile URL of the person
emailstringConditionalEmail address of the person
firstNamestringConditionalFirst name
lastNamestringConditionalLast name
companyDomainstringNoDomain of the person's company, such as acme.com
companyNamestringNoName of the person's company, when you have no domain
externalIdstringNoYour own id for the item, up to 255 characters, given back in its row of the results

Give each item a Social profile URL, an email, or a first and last name with its company: these find the right person. An item that holds only a company is still looked up, rarely matches anyone, and costs nothing when no profile is found. An item with no field at all reads skipped in the results, and an item whose only field is an invalid profile URL reads invalid_input: neither costs anything. With only contact_email or contact_phone, an item the finders cannot reach, such as an email alone for contact_email, reads skipped too. A request in which no item can be looked up returns 422 BULK_JOB_CSV_PARSE_ERROR and nothing is created.

Unknown fields

A field that the body or an item does not define is refused with 422 UNKNOWN_FIELD, never ignored: the job would otherwise run without it, for example a profile-only job for a misspelled families. error.details names the field, such as items[3].linkedin_url, the field it most likely stands for, and the accepted fields. Nothing is created and nothing is charged.

Idempotency-Key header

Creating a job reserves its credits. If the response gets lost (a timeout, a dropped connection), sending the request again would create and charge a second job. To retry safely, send an Idempotency-Key header with a key you choose for this job, such as a UUID, and send the same key when you retry.

For 24 hours, the same key with the same body returns the first response with the header Idempotent-Replayed: true, without creating another job or charging anything. The same key with another body returns 422 IDEMPOTENCY_KEY_REUSED, and a key whose first request is still being processed returns 409 IDEMPOTENCY_KEY_IN_USE: retry after the Retry-After seconds. A key that is empty, longer than 255 characters, or holds a space or a non-ASCII character returns 400 INVALID_IDEMPOTENCY_KEY.

Response structure

The request answers 202 at once, with a Retry-After: 2 header.

FieldTypeDescription
successbooleantrue if the job was created
errornullAlways null on success
dataobject

The job created

Show child attributes
FieldTypeDescription
jobobjectThe job, with the fields of Get a job. Save its id: it reads, downloads and cancels the job.
duplicateCountnumberItems that name the same person as an earlier item: looked up once, free
creditsEstimatednumberCredits reserved for the job: the most its items can cost
notRunForCreditsnumberItems not run because your balance did not cover them

quotas and metadata are the blocks of every V2 response: see Usage. quotas.creditsConsumed is 0, and quotas.workspace.credits is read before the job reserves its credits: creditsEstimated is what the job took, and Usage gives the balance after it.

Then read the job at job.links.self while its status is queued or running: see Get a job.

Webhook callback

With webhookUrl, that URL receives one POST when the job settles: it completed, was cancelled, or failed. It receives nothing while the job runs. The event says which job settled and carries no results: download them at resultsUrl with your API key.

This event is not the lookup callback of the Webhooks guide: it has its own payload, below, and is sent to webhookUrl only, never to the workspace webhook URL of Settings > Webhooks.

FieldTypeDescription
eventstringAlways job.updated
jobIdstringThe job that settled
typestringAlways enrich_persons
statusstringcompleted, cancelled or failed
pollUrlstringPath that reads the job
resultsUrlstring | nullPath of the results file, null for a failed job
occurredAtstringWhen the event was sent (ISO 8601)

The request is JSON and its x-reversecontact-event header names the event, job.updated. Answer with a 2xx within 10 seconds. A timeout, a network error, a 408, a 429 or a 5xx is tried again after 2 then 5 seconds. If these three tries fail, the delivery is tried again 1, 2, 4, 8, 16 and 32 minutes later, then twice an hour apart: about 3 hours in all. A redirect is never followed, and a redirect or another 4xx is not tried again. A retried delivery can reach you after your endpoint already handled it, so the same event can arrive twice: key it on jobId and status. If the event has not arrived, read the job at pollUrl.

The URL must be public HTTPS: a private, local or unresolvable host returns 400 WEBHOOK_URL_UNREACHABLE and nothing is created. It is checked again before each delivery.

Errors

StatusCodeMeaning
400WEBHOOK_URL_UNREACHABLEwebhookUrl is not a public HTTPS URL
402BULK_JOB_INSUFFICIENT_CREDITSYour balance does not cover a single item
403INSUFFICIENT_PERMISSIONSThe key lacks the permission of a lookup the job runs
403BULK_JOB_PHONE_FINDER_DISABLEDcontact_phone is asked on a workspace where phone finding is not enabled
403KEY_CREDIT_LIMIT_REACHEDThe job would take the key over its total credit limit
413REQUEST_BODY_TOO_LARGEThe JSON body is over 25 MB
422UNKNOWN_FIELDThe body or an item holds a field the job does not define
422VALIDATION_ERRORA required field is missing, a value is not accepted, or more than 25,000 items
422BULK_JOB_CSV_PARSE_ERRORNo item has a Social profile URL, an email or a name the selected families can look up
429KEY_CREDIT_LIMIT_REACHEDThe job would take the key over its daily or monthly credit limit
503BULK_QUEUE_UNAVAILABLEThe job queue is busy: retry shortly, with the same Idempotency-Key

The Idempotency-Key errors are listed in Idempotency-Key header.

Code examples

Code Examples

curl -X POST https://api.reversecontact.com/v2/enrich/persons/bulk \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9e2f4a1c-6b3d-4e7f-8a5c-2d1b0e9f8a7c" \
  -d '{
    "name": "CRM export, November",
    "items": [
      { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" },
      { "firstName": "John", "lastName": "Smith", "companyDomain": "acme.com", "externalId": "crm-002" }
    ],
    "families": ["profile", "contact_email"],
    "webhookUrl": "https://example.com/hooks/reversecontact"
  }'

Example response

Example Response

{
  "success": true,
  "data": {
    "job": {
      "id": "7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60",
      "type": "enrich_persons",
      "entity": "persons",
      "status": "queued",
      "source": "api",
      "name": "CRM export, November",
      "listId": null,
      "progress": {
        "total": 2,
        "processed": 0,
        "succeeded": 0,
        "failed": 0,
        "skipped": 0,
        "remaining": 2
      },
      "credits": { "estimated": 9, "used": null },
      "estimatedSecondsRemaining": null,
      "error": null,
      "createdAt": "2026-11-04T09:30:00.000Z",
      "startedAt": null,
      "completedAt": null,
      "links": {
        "self": "/v2/jobs/7b1e4c2a-5d3f-4e8b-9a6c-1f2d3e4b5a60",
        "results": null,
        "operation": null
      }
    },
    "duplicateCount": 0,
    "creditsEstimated": 9,
    "notRunForCredits": 0
  },
  "error": null,
  "quotas": {
    "creditsConsumed": 0,
    "workspace": {
      "id": "ws_abc123",
      "credits": { "total": 10000, "used": 4522, "left": 5478 }
    }
  },
  "metadata": {
    "requestId": "req_7f3c9a2e1b4d",
    "executionTimeMs": 120
  }
}