Integration guides
Lists: orchestrate enrichment
NewLast verified
A List orchestrates identity, deduplication, data and contact lookups for up to 25,000 people or companies at a time, under one budget, with one durable result shared with the app and your agents.
Lists are the centre of the Reverse Contact API. Every other endpoint is a single lookup: one person or one company in, one answer out, and your code chains the lookups, follows the async ones, retries, removes duplicates, counts credits and stores the results. A List orchestrates that whole pipeline for you, for up to 25,000 people or companies at a time. You hand it what you know about them: it identifies each one, buys the data you ask for under a budget you set, keeps the result, and tells you when it is done.
What a List orchestrates
For each line you send, a List runs the chain you would otherwise write yourself:
- Identity. It picks the lookup each Signal needs. A Social profile URL is read from saved profiles, or live with
live: true. An email, or a name with its company, is reversed to the person behind it. A domain, a company page URL or a company id leads to the company. A Signal no one matches is kept as Pending Intake, for free. - Deduplication. A person or a company already on the List is recognised and costs nothing, and lines naming the same identity with the same
externalIdare merged into one. - Data. It buys the data families you ask for (profile, company details, work email, phone number) only for the members that lack them. The lookup that adds a member already stores its profile, or its company details on a companies List, and the email of an email Signal becomes the member's work email, for free. Data a member already holds is never bought twice, and an email or a phone number already looked for is not looked for again.
- Budget. It never spends more than the
maxCreditsyou set. Before a step that would cross it, or when your balance runs out, it pauses, keeps the work done, and waits for you to resume. - Execution. The import runs in the background as one List Operation: free progress reads with long polling, a webhook each time it stops moving, a result for each line with your own
externalId, and a cancel that never charges the lines not started. - Storage. Members and their data stay in the List, ready to read, to find by your own id, to export as CSV, or to grow with the next import.
One set of Lists for the app, your code and your agents
A List belongs to your workspace, not to the API. A List created with the API shows up in the app for your team. A List built in the app, from a Search or a file, can be read, filled and exported through the API. Ask, in the app, and your own agent, through the MCP server, and your code, through the API or the SDKs, work on the same Lists with the same rules. However a member entered, it is one member, charged once.
Single endpoints or a List
| What you have to handle | With single endpoints | With a List |
|---|---|---|
| Many people at once | One request per person | Up to 25,000 lines in one import |
| Signals of different kinds | Your code picks the endpoint for each row | The List picks the lookup for each Signal |
| Emails and phone numbers | One async job to follow per lookup | One operation for the whole import |
| Duplicates | Your code detects them | A member already on the List is free |
| Spending | Your code counts credits | maxCredits, with pause and resume |
| Failures and retries | Your code retries, and must not pay twice | Send the import again with its operationId: nothing is charged twice |
| The result | Your code stores it | Kept in the List, shared with the app and your agents |
A single endpoint stays the right tool to look up one person or one company while a user waits: Enrich profile, Person profile or Find Email. Work on a set of people or companies you keep (a CRM segment, a prospect list that grows every week, target accounts to hand to your sales team) belongs in a List.
A List or Bulk enrichment
Bulk enrichment also takes up to 25,000 people in one request, without a List: it looks up each one, gives you a CSV with one row per item, and keeps nothing else. Choose by what you do with the result:
| You want to | Use |
|---|---|
| Enrich a file of people once and get it back as CSV | Bulk enrichment |
| Keep the people, add to them and buy data over time | A List |
| Never pay twice for someone you already have | A List |
| Work on companies, or buy company details | A List |
Cap the spend with maxCredits, pause and resume | A List |
The job history lists both: your Bulk enrichment jobs and the jobs of your Lists, such as imports, with their progress and credits.
The building blocks of a List
| Term | What it means |
|---|---|
| List | A named set of people (persons) or of companies (companies) in your workspace, never both |
| Signal | What you know about someone: a Social profile URL, an email, a first and last name with a company, a company domain |
| List Member | A person or a company a Signal matched, with the data bought for it and your externalId |
| Pending Intake | The Signals no one matched, kept on the List for free so you can see what was not found |
| Data family | Data you buy for each member: profile, company, contact_email, contact_phone |
| List Operation | One run of work on a List, such as an import, with an id, a status and counters, followed at its pollUrl or told to your webhookUrl |
The API flow, endpoint by endpoint
- Create the List with Create a List: a name, and
personsorcompanies. - Fill it with Signals. Add members takes up to 1,000 Signals and answers when they are done. Import members and data queues up to 25,000 Signals or a public CSV file, and can buy data families for each member.
- Follow the import with Read a List operation, the
pollUrlof the import, or be told by itswebhookUrl. - Read the result with List members, or download it with Export a List.
Every call of this flow is free. You pay only for the members it adds and the data it buys (see What a List costs). Build your first List runs this flow step by step, with cURL, JavaScript, Python and the SDKs.
The API key needs the lists permission. A call that spends credits also needs the permission of what it buys, as the matching endpoint would: persons:enrich, companies:enrich or contact (see API key management).
Add members or run an import
| You want to | Use |
|---|---|
| Add a few hundred people and get the result in the reply | Add members |
| Add more than 1,000 Signals, or a public CSV or Google Sheet | Import members and data |
| Buy emails, phone numbers, profiles or company details for each member | Import members and data with families |
| Cap what a run may spend | Import members and data with maxCredits |
| Be told when the work stops moving | Import members and data with webhookUrl |
An import of a CSV file sends its public URL in sourceUrl instead of signals: up to 25,000 rows and 25 MiB, with the columns matched from their headers (Social URL, email, first name, last name, company, domain).
Grow a List and buy data over time
A List is built for work that lasts: add a file today, buy emails for its members tomorrow, add the new leads of next week.
- Add new people at any time. Send them with Add members or an import: a person already on the List reads
already_presentand costs nothing. - Buy data later. To buy data for members already on a List, import them again with
families: their identity costs nothing, and only the data they lack is bought. An email or a phone number already looked for, found or not, is not looked for again. - Take people out with Remove members, which also removes the data stored for them, and delete a List you no longer need with Delete a List.
- Imports run in turn. An import queued behind a paused one on the same List waits until you resume or cancel it (
stopReason: "blocked_by_paused_operation"). Deleting a List returns409 LIST_BUSYwhile an operation is queued, running or paused on it, while the lines of a cancelled one still wait for their result, or while a Search of the app is still adding people to it.
Link each member to your CRM with externalId
Send externalId on each Signal, the id of the line in your CRM or file. Each line result gives it back, the member keeps it, and List members finds a member by it (?externalId=crm-001). You never have to store our ids to find the member of a line. A member keeps the first externalId it received, and externalId is never used to find anyone.
Pending Intake
A Signal no one matches does not become a member: the List keeps it as a Pending Intake line, free, so you can see what was not found and fix it. Rows of Get a List and List members carry kind: "pending" for those lines and kind: "member" for members. Pending Intake is never enriched, and Export a List writes its lines with what was sent. Send a Signal again once it is fixed: when it finds someone, the new member replaces its Pending Intake line. Dismiss lines with the pendingIds of Remove members.
Control spend: maxCredits, pauses and resume
An import with families whose worst case is above 25 credits needs maxCredits, the most it may spend. The import pauses before a step that would cross it, keeps the work done, and reports stopReason: "max_credits" with the smallest ceiling that lets it continue in nextStep.minimumMaxCredits. A balance that runs out pauses it the same way, with stopReason: "balance". An import sent without maxCredits may spend up to its worst case.
To resume, send the same import again, same operationId, with a higher maxCredits or after adding credits: nothing already done is charged twice. To stop an import for good, cancel it with Cancel a List operation: the lines that had not started are never charged. See Pauses and how to resume.
What a List costs
| What | Credits |
|---|---|
| Create, read, export, remove, delete, follow and cancel | Free |
| A new person | 1 from a Social profile URL (2 with live: true), 2 from an email or a name |
| A new company | 1 from a company page URL or a company id (2 when the page is read live), 2 from a domain |
| A member already on the List, a Signal no one matches | Free |
profile family on a people List, company family on a companies List | Per member that lacks it, 1 when read from its Social profile or company page URL, 2 when found by name or domain (2 in the worst case). A member added from a Signal already holds it, for free |
company family on a people List | Per member, 1 from the company page URL of the person's current company, 2 from its domain (2 in the worst case) |
contact_email family | 3 per email found, free when none is found. The email of an email Signal is kept for free |
contact_phone family | 20 per phone number found, free when none is found |
Limits per workspace and per List
| Limit | Paid plan | Free trial |
|---|---|---|
| Lists per workspace | 200 | 1 |
| Members per List | 25,000 | 50 |
| Pending Intake lines per List | 25,000 | 50 |
| Signals per Add members call | 1,000 | 1,000 |
| Signals or CSV rows per import | 25,000 | 25,000 |
The Lists of a free trial that ends without a purchase are deleted 30 days after the trial ended.
The List calls count toward your per-minute and daily rate limits like any other call, with two exceptions: reading an operation is never counted, and the request that starts an import counts once on the daily limit and not on the per-minute one, whatever its number of lines. The lines of the import 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.
Error codes of the List endpoints
The List endpoints answer with the error envelope of every V2 endpoint (see Error handling & retries). Their own codes:
| Status | Code | Meaning |
|---|---|---|
400 | LIST_INVALID_INPUT | A field is missing or does not fit the List: the message says which |
400 | LIST_ENTITY_MISMATCH | A company sent to a people List, or a person to a companies List, with Add members. An import marks the line refused instead. |
400 | WEBHOOK_URL_UNREACHABLE | The import webhookUrl is not a public HTTPS URL |
402 | INSUFFICIENT_CREDITS | The balance cannot pay for what Add members could add |
403 | INSUFFICIENT_PERMISSIONS | The key lacks lists, or the permission of what the call buys |
404 | LIST_NOT_FOUND | No List with this id in your workspace |
404 | LISTS_NOT_ACTIVATED | The workspace has never had a List: create one first |
404 | LIST_OPERATION_NOT_FOUND | No operation with this id on this List |
409 | LIST_LIMIT_REACHED | The workspace already has its maximum of Lists |
409 | LIST_MEMBER_LIMIT_REACHED | The List already holds its maximum of members |
409 | LIST_PENDING_INTAKE_LIMIT_REACHED | The List already holds its maximum of Pending Intake lines |
409 | LIST_BUSY | The List you delete still has work on it: an operation queued, running or paused, the last lines of a cancelled one, or a Search of the app adding people. Wait for it or cancel it |
409 | LIST_OPERATION_NOT_CANCELLABLE | The operation already ended |
409 | IDEMPOTENCY_KEY_REUSED | The Idempotency-Key already created a List of the other kind |
422 | VALIDATION_ERROR | A field has the wrong type or format, such as an id that is not a UUID |
422 | BULK_JOB_CSV_PARSE_ERROR | The sourceUrl file of an import could not be read |
429 | LIST_EXPORT_IN_PROGRESS | The workspace is already exporting a List: retry after Retry-After |
429 | LIST_EXPORT_BUSY | The server is serving another export: retry after Retry-After |
502 | LIST_JOB_ENGINE_ERROR | The import could not be queued: send it again with the same operationId |
503 | LISTS_PREPARING | The first List of the workspace is being set up: retry in a few seconds |
503 | BULK_QUEUE_UNAVAILABLE | The import queue is busy: retry shortly |
504 | GATEWAY_TIMEOUT | The request ran out of time: see Add members and Import members and data |
FAQ
What is a List in the Reverse Contact API?
A List is a named set of people, or of companies, kept in your Reverse Contact workspace, and the part of the API that orchestrates the work on them. From Signals (Social URLs, emails, names with a company, company domains) it identifies each person or company, skips those already on the List, buys the data families you ask for under a maxCredits budget and keeps the result, which you read with GET /v2/lists/:listId/members or download with GET /v2/lists/:listId/export. It is the same List your team sees in the app.
When should I use a List instead of Enrich profile?
Use POST /v2/enrich/persons to look up one person while a user waits. Use a List to enrich a file or a CRM segment of hundreds or thousands of contacts, to buy emails or phone numbers for a set of people under a maxCredits cap, or to keep a set of prospects that grows over time: one List import takes up to 25,000 lines, runs in the background, and never charges twice for a person already on the List.
How much does it cost to add someone to a List?
A new person costs 1 credit from a Social profile URL read from saved data, 2 with live: true, and 2 from an email or a name. A person already on the List and a Signal no one matches are free, and so are creating, reading, exporting and deleting Lists.
How do I find emails for everyone on a List?
Send the members again to the import endpoint, POST /v2/lists/:listId/intake/signals, with families: ["contact_email"]. People already on the List cost nothing for their identity, each email found costs 3 credits, and a person without an email costs nothing. A person whose email was already looked for, found or not, is not looked up again, and the email of an email Signal becomes the person's work email for free. Above 25 credits of worst case, the import needs maxCredits.
What happens to a Signal that matches no one in a List?
A Signal no one matches is kept on the List as a Pending Intake line, for free, and never enriched. Get a List and List members return those lines with kind: "pending", and Export a List writes them with what was sent. Send the Signal again once it is fixed: when it finds someone, the new member replaces its Pending Intake line. Dismiss the lines you do not need with the pendingIds of DELETE /v2/lists/:listId/members.