Skip to content

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

Integration guides

Build your first List

New

Last verified

Turn CRM contacts into a List, buy their work emails under a credit cap, follow the import and read the result, with cURL, JavaScript, Python and the SDKs.

This guide turns three contacts of a CRM into a List, finds their work emails, and reads them back. It takes five calls, all free: you pay only for the members the List adds and the emails it finds, 11 credits at most here.

Before you start

  • An API key in RC_API_KEY, from API Keys. A key with full access works. A scoped key needs lists, plus Person Enrichment for the people the List adds and Contact for the emails it buys (see API key management).
  • A few credits. A free trial holds one List of up to 50 members, enough for this guide.

The steps

1. Create the List

cURL

POST /v2/lists

curl -X POST https://api.reversecontact.com/v2/lists \
  -H "Authorization: Bearer $RC_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: q4-sales-leaders" \
  -d '{"name": "Q4 sales leaders", "entity": "persons"}'

The response gives the List in data: keep data.id, the List id the next calls name in their path ($LIST_ID in the commands below). The Idempotency-Key makes the call safe to retry: sent again, it returns the same List instead of a second one.

2. Import the people and buy their emails

cURL

POST /v2/lists/:listId/intake/signals

curl -X POST https://api.reversecontact.com/v2/lists/$LIST_ID/intake/signals \
  -H "Authorization: Bearer $RC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
    "signals": [
      { "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" },
      { "email": "john.smith@acme.com", "externalId": "crm-002" },
      { "firstName": "Alex", "lastName": "Martin", "companyDomain": "acme.com", "externalId": "crm-003" }
    ],
    "families": ["contact_email"],
    "maxCredits": 50
  }'

Each Signal says what you know about one person, and externalId carries the id of the line in your CRM. families asks for a work email for each member that has none yet. The import answers at once with data.operationId and data.pollUrl, and runs in the background.

The worst case of this import is 14 credits: 1 for Jane Doe found from her Social profile URL, 2 each for John Smith found from his email and Alex Martin found from his name, and 3 per email found. John Smith's email is the one you sent: it becomes his work email for free and is not looked up, so this import charges at most 11 credits. A line no one matches and an email not found cost nothing. Up to 25 credits of worst case, maxCredits is optional: without it, the import may spend up to its worst case. Send it anyway to set your own cap.

operationId is a UUID you choose. Keep it: sending the same import again with it never charges twice, and it is how you resume an import that paused.

3. Follow the import

cURL

GET /v2/lists/:listId/operations/:operationId/progress

curl "https://api.reversecontact.com/v2/lists/$LIST_ID/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress?wait=20" \
  -H "Authorization: Bearer $RC_API_KEY"

This route is the pollUrl the import answered with, built from the List id and your operationId. With wait=20, each read waits up to 20 seconds for the import to complete, pause or stop, and otherwise answers with the import still running. Read it again while clientAction is poll. Then done with status: "completed" means the members and their emails are in the List, and resume means it paused: nextStep says what to do. These reads are free and never counted against your rate limits. Instead of reading, you can pass a public HTTPS webhookUrl with the import: it is told each time the operation stops moving.

4. Read the members and their emails

cURL

GET /v2/lists/:listId/members

curl "https://api.reversecontact.com/v2/lists/$LIST_ID/members?kind=members" \
  -H "Authorization: Bearer $RC_API_KEY"

curl "https://api.reversecontact.com/v2/lists/$LIST_ID/members?externalId=crm-002" \
  -H "Authorization: Bearer $RC_API_KEY"

Each member carries its fields (name, headline, company, location), your externalId, and families.contactEmail with the email found, or for John Smith the email you sent (source: "signal:paste"). The second call finds the member of a CRM line by its externalId, without storing any id of ours.

5. Or download the List as CSV

cURL

GET /v2/lists/:listId/export

curl -o q4-sales-leaders.csv "https://api.reversecontact.com/v2/lists/$LIST_ID/export" \
  -H "Authorization: Bearer $RC_API_KEY"

The file has one row per line of the List, members and Pending Intake, in the order they were added, with the data the List holds. Exporting is free and never looks anyone up again. A workspace exports one List at a time: run your exports one after the other, a second one at the same time returns 429 LIST_EXPORT_IN_PROGRESS.

The complete pipeline

The same five steps in one script, with plain HTTP calls or with the TypeScript and Python SDKs.

Create, import, follow, read

const BASE = "https://api.reversecontact.com";
const headers = {
  "Authorization": `Bearer ${process.env.RC_API_KEY}`,
  "Content-Type": "application/json"
};

// 1. Create the List: the Idempotency-Key makes a retry safe
const list = await fetch(`${BASE}/v2/lists`, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "q4-sales-leaders" },
  body: JSON.stringify({ name: "Q4 sales leaders", entity: "persons" })
}).then((res) => res.json());
const listId = list.data.id;

// 2. Import the people and buy their emails, 50 credits at most
const queued = await fetch(`${BASE}/v2/lists/${listId}/intake/signals`, {
  method: "POST",
  headers,
  body: JSON.stringify({
    operationId: crypto.randomUUID(), // keep it to resume the import
    signals: [
      { linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" },
      { email: "john.smith@acme.com", externalId: "crm-002" },
      { firstName: "Alex", lastName: "Martin", companyDomain: "acme.com", externalId: "crm-003" }
    ],
    families: ["contact_email"],
    maxCredits: 50
  })
}).then((res) => res.json());

// 3. Follow the import until it stops moving
let operation;
do {
  const res = await fetch(`${BASE}${queued.data.pollUrl}?wait=20`, { headers });
  operation = (await res.json()).data;
} while (operation.clientAction === "poll");
if (operation.status !== "completed") throw new Error(operation.nextStep?.message ?? `Import ${operation.status}`);

// 4. Read the members and their emails
const members = await fetch(`${BASE}/v2/lists/${listId}/members?kind=members`, { headers }).then((res) => res.json());
for (const member of members.data.items) {
  console.log(member.externalId, member.fields.name, member.families.contactEmail?.value ?? "no email");
}

A List holds more than one page of members: send nextCursor back as cursor to read the next one (see List members). The SDK iterators read every page for you.

Import a CSV file instead

Send the public URL of a CSV file or a Google Sheet in sourceUrl, in place of signals: up to 25,000 rows and 25 MiB. Columns are matched from their headers (Social URL, email, first name, last name, company, domain), and each row becomes a Signal.

cURL

Import a CSV file

curl -X POST https://api.reversecontact.com/v2/lists/$LIST_ID/intake/signals \
  -H "Authorization: Bearer $RC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "operationId": "7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b",
    "sourceUrl": "https://example.com/exports/q4-leads.csv",
    "families": ["contact_email"],
    "maxCredits": 2000
  }'

The answer adds sourceRows, the data rows read from the file, and mapping, the header each field was read from: check it before you buy anything more. A file that is not public can be sent as signals instead, one object per row, with the id of the row in externalId.

What to do next

  • Add the new leads of next week to the same List: a person already on it costs nothing (see Grow a List and buy data over time).
  • Buy phone numbers or full profiles later, for the members that lack them, with the same import and other families.
  • See what was not found in Pending Intake, and how an import pauses and resumes in Control spend.
  • Hand the work to an AI agent: the MCP server runs the same steps on the same Lists.

FAQ

How much does it cost to build a List of CRM contacts with their work emails?

Creating, following, reading and exporting a List are free. Each new person costs 1 credit from a Social profile URL and 2 from an email or a name, and each work email found costs 3 credits. The email of a contact sent by email becomes its work email for free. For the three contacts of this guide, the import costs at most 11 credits, and a contact no one matches or an email not found costs nothing.

Can I send the same List import twice without paying twice?

Yes. Send the import to POST /v2/lists/:listId/intake/signals with the same operationId: the API returns the operation it already created and never charges anything twice. The same operationId also resumes an import that paused at its maxCredits or at a low balance.

How do I find the member of a CRM contact in a List?

Send the id of the CRM contact as externalId on its Signal. The member keeps it, and GET /v2/lists/:listId/members?externalId=crm-001 returns that member, so you never have to store a Reverse Contact id.

How do I know when a List import is finished?

Read GET /v2/lists/:listId/operations/:operationId/progress, the pollUrl of the import, with wait=20 until clientAction is no longer poll: done means it ended, resume means it paused and nextStep says what to do. These reads are free and not counted against your rate limits. You can also pass a public HTTPS webhookUrl with the import, which is told each time the operation stops moving.

Can I build a List with the Reverse Contact SDK?

Yes. From version 0.2.0, the TypeScript SDK @reverse-contact/sdk and the Python SDK reversecontact manage Lists with client.lists: create, importSignals (import_signals in Python), operations.wait to follow an import, and members.listAll (members.list_all) to read every member.