Skip to content

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

Integration guides

Enrich people in bulk

New

Last verified

Send up to 25,000 people in one request, follow the job, read the results as they come or download one row per person as CSV or NDJSON, with their profile, work email or phone number, priced like the unit calls and refunded for what is not found.

Bulk enrichment looks up a whole file of people in one request. You send up to 25,000 people as JSON, each with a Social profile URL, an email, or a name with its company, and choose what to find for them: the profile, a work email, a phone number. The job runs in the background, and you get one row per person, in the order you sent them, with your own id on each row: read them as they come, or download them as a CSV or NDJSON file.

It is the API version of Bulk Import in the app: the same lookups, the same prices, the same CSV.

When to use Bulk enrichment

You want toUse
Look up one person while a user waitsEnrich profile or Find Email
Enrich a file or a CRM export once and get it back as CSVBulk enrichment
Keep the people, add to them over time, never pay twice for someoneA List
Work on companies, or cap the spend with maxCreditsA List
Enrich a CSV without writing codeBulk Import in the app

Bulk enrichment keeps nothing once you have the file. If you will come back to the same people, to buy their phone numbers next month or to add next week's leads, start with a List instead.

How a job runs

  1. Start the job with Bulk enrichment, POST /v2/enrich/persons/bulk. It answers 202 at once with the job.
  2. Follow it with Get a job, GET /v2/jobs/:jobId?wait=20, while its status is queued or running. Or pass a webhookUrl: it receives a job.updated event when the job settles.
  3. Get the results: read them a page at a time while the job runs with Read job results by page, GET /v2/jobs/:jobId/rows, or download them once it settles with Download job results, GET /v2/jobs/:jobId/results: one row per item, as CSV or NDJSON.
  4. Stop it if you need to with Cancel a job, and find any past job in the Job history.

Starting, following, reading the results and cancelling are free. You pay only for what the items find.

What it costs

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
Profile, from an email or a name2
contact_email3 for a professional email, 5 for a personal one, 8 for both with emailType: "any"
contact_phone20 per phone number found

When the job starts, 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: its row reads insufficient_credits, it 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, so you pay only for what was found.

  • An item that names the same person as an earlier one is looked up once: its row reads duplicate and costs nothing.
  • The email and phone finders start from a Social profile URL, or from a first name, a last name and a company domain. Any other item first has its profile matched, charged like a profile lookup when it is found.
  • On a Go, Plus or Max plan, a profile your plan covers costs no credit, as in its unit call. Once the workspace has delivered its daily profile cap, the items of the job wait until 00:00 UTC instead of being refused.
  • A key with credit limits must hold the whole reservation, and counts only what the job spent once it settles.

Step by step

1. Prepare the items

Each item takes a linkedinUrl, an email, or a firstName and lastName with a companyDomain (or a companyName when you have no domain). Add externalId, the id of the line in your CRM or file: it comes back on the row of the item. An item with no field at all is not run and reads skipped, and an item that holds only a company is looked up but rarely matches anyone.

Then choose families: profile (the default), contact_email, contact_phone, or several. With contact_email, emailType picks professional (the default), personal or any.

2. Start the job

cURL

POST /v2/enrich/persons/bulk

curl -X POST https://api.reversecontact.com/v2/enrich/persons/bulk \
  -H "Authorization: Bearer $RC_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"]
  }'

The answer gives the job in data.job: keep its id ($JOB_ID in the commands below). creditsEstimated is what the job reserved, duplicateCount the items looked up once, and notRunForCredits the items your balance did not cover.

Send an Idempotency-Key you choose for the job. If the answer gets lost, send the same request with the same key: for 24 hours, it returns the job it created instead of reserving credits for a second one.

3. Follow the job

cURL

GET /v2/jobs/:jobId

curl "https://api.reversecontact.com/v2/jobs/$JOB_ID?wait=20" \
  -H "Authorization: Bearer $RC_API_KEY"

With wait=20, each read waits up to 20 seconds for the job to settle. Read it again while status is queued or running, with an HTTP timeout above 20 seconds. These reads are free, also at a zero balance, and never counted against your rate limits. progress counts the items done, and credits.used is set once the job settles.

Instead of reading, you can pass a public HTTPS webhookUrl when you start the job: it receives one job.updated event when the job completes, is cancelled or fails, with the path of the results. A failed delivery is tried again for about 3 hours, so the same event can arrive twice: key it on jobId and status. Read the job if the event has not arrived.

4. Download the results

cURL

GET /v2/jobs/:jobId/results

curl -o results.csv "https://api.reversecontact.com/v2/jobs/$JOB_ID/results" \
  -H "Authorization: Bearer $RC_API_KEY"

The file is ready once the job is completed or cancelled: links.results of the job is set then. It holds one row per item, in the order you sent them: your fields and externalId first, then rc_status, rc_message, and the columns of what you asked for, such as rc_email or the rc_ profile columns. See Download job results for every column. The file is UTF-8 with a byte order mark, so a spreadsheet opens accented names correctly.

For a program, add format=ndjson: the file then holds one JSON object per item, with the person in the shape of Enrich profile.

cURL

GET /v2/jobs/:jobId/results?format=ndjson

curl -o results.ndjson "https://api.reversecontact.com/v2/jobs/$JOB_ID/results?format=ndjson" \
  -H "Authorization: Bearer $RC_API_KEY"

Or read the results as they come

You do not have to wait for the job to settle: Read job results by page returns the same JSON objects, up to 100 per page, in the order sent, from the start of the run. Pass nextCursor back as cursor until it is null. While the next item still runs, the page is empty: read the same cursor again after the Retry-After seconds. Each page counts toward your rate limits, and the items of a running job already take up to 80% of your per-minute limit: read with limit=100, at the Retry-After pace.

cURL

GET /v2/jobs/:jobId/rows

curl "https://api.reversecontact.com/v2/jobs/$JOB_ID/rows?limit=100" \
  -H "Authorization: Bearer $RC_API_KEY"
# The next page: pass back data.nextCursor of the previous one.
curl "https://api.reversecontact.com/v2/jobs/$JOB_ID/rows?limit=100&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $RC_API_KEY"

The complete pipeline

Start, follow, download

import { writeFile } from "node:fs/promises";

const BASE = "https://api.reversecontact.com";
const headers = { "Authorization": `Bearer ${process.env.RC_API_KEY}` };

// 1. Start the job: keep the Idempotency-Key to retry safely
const created = await fetch(`${BASE}/v2/enrich/persons/bulk`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify({
    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"]
  })
});
let { data: { job } } = await created.json();

// 2. Follow it while it is queued or running
while (job.status === "queued" || job.status === "running") {
  const response = await fetch(`${BASE}${job.links.self}?wait=20`, { headers });
  job = (await response.json()).data;
}

// 3. Download the results: write the bytes, byte order mark included
if (job.links.results) {
  const response = await fetch(`${BASE}${job.links.results}`, { headers });
  await writeFile("results.csv", Buffer.from(await response.arrayBuffer()));
}
console.log(job.status, `${job.credits.used} credits`);

Handle the results

rc_status says what happened to each row, and what to do with it:

rc_statusWhat to do
enriched, partialUse the row. partial found part of what you asked, such as the profile but no email
not_foundNothing to retry: only a profile matched on the way to a finder was charged
duplicateRead the row of the earlier item that names the same person
insufficient_creditsTop up, then send these items again in a new job
rate_limited, errorSend these items again later: they cost nothing
skippedNot run: the item had nothing to look up, or the job was cancelled before it. Complete the item, or send it again
invalid_inputFix the item, such as an invalid Social profile URL, then send it again
data_subject_blockedDrop the person: they asked not to be processed under GDPR or CCPA
unknownThe lookup may have run but its result was not saved. It is not run again on its own
runningOnly in the file of a cancelled job: the item was still running. It ends within the hour: download the file again for its outcome

To retry, filter the rows by rc_status and send their items in a new job, with a new Idempotency-Key. externalId ties each new row back to your file.

Cancel a job and read the history

Cancel a job stops a job an API key or an AI assistant started: items already in flight may finish and are charged as usual, and items not started are never charged and read skipped. The results of the items done stay downloadable. An item in flight ends within the hour: until then, its row of the file reads running, and a page of Read job results by page waits for it. A job started by a person in the app or with Ask is stopped there.

The Job history lists every job of your workspace, newest first, whoever started it: your Bulk enrichment jobs, the files enriched with Bulk Import in the app, the imports and enrichments of your Lists, and the Search exports. Filter it by type, status or List.

Limits

LimitValue
Items per request25,000
Request body25 MB of JSON, above it 413 REQUEST_BODY_TOO_LARGE
Rate limitsStarting a job counts as one request, whatever its number of items. Its items then take at most 80% of your per-minute limit as they run, and wait when that share is taken
Following a jobFree and never counted against your rate limits
Downloading the resultsFree, counted like any other call
Reading the results by pageUp to 100 items and 2 MB of JSON per page. Free, each page counted like any other call

A larger file goes in several jobs. 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. See Rate limits for how a job shares the per-minute limit with your other calls.

FAQ

How many people can I enrich in one Bulk enrichment request?

POST /v2/enrich/persons/bulk takes up to 25,000 people per request, in a JSON body of at most 25 MB. Each person is found best from a Social profile URL, an email, or a first and last name with a company. Split a larger file into several jobs.

How much does Reverse Contact Bulk enrichment cost?

Each person costs what its unit call costs, only for what is found: 1 credit for a profile from a Social profile URL (2 with live: true), 2 from an email or a name, 3 for a professional email, 5 for a personal one and 20 for a phone number. The job reserves the most each person can cost when it starts and gives back what was not found. Starting, following, downloading and cancelling a job are free.

What happens when my balance does not cover every person of a Bulk enrichment job?

The job reserves credits person by person, in the order you sent them, as long as your balance covers them, and does not run the people it no longer covers: their rows read insufficient_credits in the results and cost nothing, and notRunForCredits counts them. Top up, then send those people again in a new job.

How do I know when a Bulk enrichment job is finished?

Read GET /v2/jobs/:jobId?wait=20 while status is queued or running: each read waits up to 20 seconds and is free. The job is finished when status is completed, cancelled or failed. You can also pass a public HTTPS webhookUrl when you start the job: it receives a job.updated event once the job settles. The results file, CSV or NDJSON, is then at GET /v2/jobs/:jobId/results. To use the results before the job finishes, read them a page at a time with GET /v2/jobs/:jobId/rows.

Should I use Bulk enrichment or a List?

Use Bulk enrichment to enrich a file of people once and get it back as CSV: nothing is kept. Use a List to keep people or companies in your workspace, add to them over time, buy their data later and never pay twice for someone already on the List, with a maxCredits cap on each import.