Skip to content

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

Endpoints // Lists

Import members and data

New

Last verified

Available inAPIMCPSDK

Queue up to 25,000 Signals or a public CSV into a List, buy profile, company, email and phone data for each member under a credit ceiling, and follow the import by polling or webhook.

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

Credits

Free to queue.

Each new member costs its identity price, and each data family you ask for costs what its endpoint costs, up to your maxCredits.

Introduction

The Import members and data endpoint queues up to 25,000 Signals, or a public CSV file or Google Sheet, as one operation on a List. It answers at once with an operationId and a pollUrl, then works in the background: each line is identified like an add, and with families, the data you ask for is bought for each member, new or already on the List.

Use it instead of Add members for more than 1,000 Signals, to fill a List with emails, phone numbers, profiles or company details, to set a credit ceiling, or to be told by webhook when the import stops moving. Follow it with Read a List operation and stop it with Cancel a List operation.

Credits

Queuing the import is free: the operation is charged line by line as it runs, and never more than maxCredits.

What the line doesCredits
Adds a new person1 from a Social profile URL (2 with live: true), 2 from an email or a name
Adds a new company1 from a company page URL or id (2 when the page is read live), 2 from a domain
Names a member already on the List, or no oneFree
profile familyPer member that lacks it, 1 when read from its Social profile URL, 2 when found by name (2 in the worst case). A person the import adds comes with their profile, free.
company familyPer member, 1 from a company page URL, 2 from a domain (2 in the worst case). On a people List, it is the person's current company. On a companies List, a company the import adds comes with its details, free.
contact_email family3 per email found, free when none is found. The email of an email Signal is kept as the member's email, free.
contact_phone family20 per phone number found, free when none is found

Data a member already holds is never bought again, and an email or a phone number already looked for, found or not, is not looked for again: an import replayed on the same List charges only what is missing. A line no one matches is kept as Pending Intake, free. When a Signal sent again finds someone, the new member replaces its Pending Intake line.

The contact_phone family needs phone finding on your workspace, as Find Phone does. Without it, the member of each line that asks for a phone number is still added, the line reads resolved with errorCode: "CONTACT_PHONE_FINDER_DISABLED", and no phone number is charged.

The API key needs the lists permission, plus the permission of each thing the import buys: persons:enrich for people and their profiles, companies:enrich for companies and company details, contact for the email and phone families (see API key management).

The request that starts an import counts as one request on your daily limit and is not counted against your per-minute limit, whatever its number of lines. Its lines then take their share of your per-minute limit as they run, at most 80% of it, and wait when that share is taken. Reading the operation is never counted. See Rate limits.

Authorization

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

Request body

The List is named in the path: POST /v2/lists/:listId/intake/signals. Send signals or sourceUrl, not both.

NameTypeRequiredDescription
signalsarrayConditional1 to 25,000 Signals, with the fields of Add members, externalId included
sourceUrlstringConditionalA public HTTPS CSV file or Google Sheet URL instead 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).
familiesstringNoData to buy for each member: profile, company, contact_email, contact_phone. A companies List takes company only.
maxCreditsintegerConditionalThe most this operation may spend. Required with families when the worst case is above 25 credits. Omitted, the ceiling is the worst case of the import.
livebooleanNotrue fetches the profile of each Social profile URL live. Omitted or false, saved data is used. No effect on a companies List.
operationIdstringNoYour UUID for this operation. Sending the same request again with it returns the same operation and never charges twice.
webhookUrlstringNoPublic HTTPS URL told each time the operation stops moving, see Webhook callback
sourcestringNopaste (default) or file: how a Pending Intake line says it arrived

The credit ceiling

When families makes the worst case of the import (every line a new member, every family bought, every email and phone found) higher than 25 credits, the request must carry maxCredits: without it, it returns 400 LIST_INVALID_INPUT and nothing is queued. maxCredits is your consent to spend up to that amount, and the operation stops before a step that would cross it. An import without families needs no ceiling: it costs at most the identity of its lines. An import sent without maxCredits gets its worst case as ceiling, so it may spend up to that amount.

Deduplication and replays

Lines naming the same identity with the same externalId are deduplicated: duplicateCount gives how many were dropped. Two lines naming the same person under two externalId values stay two lines, and the second one reads already_present.

With operationId, sending the same request again returns the operation it created, whatever its state, and charges nothing twice. The same operationId with other Signals, families, live or source returns 400 LIST_INVALID_INPUT. A cancelled operation stays cancelled: a new run needs a new operationId.

Pauses and how to resume

The operation pauses instead of failing when it cannot go on, and keeps the work done:

  • maxCredits reached (stopReason: "max_credits"): send the same request again, same operationId, with a higher maxCredits. nextStep.minimumMaxCredits gives the smallest ceiling that lets it continue.
  • Balance too low (stopReason: "balance"): add credits, then send the same request again with the same operationId.
  • Paused from the app (stopReason: "paused_by_user"): send the same request again with the same operationId.

A paused operation holds the imports queued after it on the same List (stopReason: "blocked_by_paused_operation", with nextStep.blockingOperationId): resume or cancel it to let them start. An operation also stops for good when the List reaches its member or Pending Intake limit (list_member_limit, list_pending_intake_limit); the lines it did not reach are not charged.

Lines that name no one, or the wrong kind

A line never refuses the whole import. A line naming a company on a people List, or a person on a companies List, reads refused with errorCode: "ENTITY_MISMATCH". A Signal that identifies no one reads failed with errorCode: "LIST_INVALID_INPUT", and a row of a sourceUrl file with no usable column reads refused with errorCode: "NO_INPUT". These lines are free, and Read a List operation gives them line by line.

Response structure

The request answers 202 at once, with a Retry-After: 5 header. It runs within a 15-second time limit, which a slow sourceUrl download can cross: past it, it returns 504, and the import may have been queued all the same. Always send an operationId for this reason: the same request sent again with it returns the operation if it exists, or queues it, and never charges twice.

FieldTypeDescription
successbooleantrue if the operation was queued
errornullAlways null on success
dataobject

The operation queued

Show child attributes
FieldTypeDescription
operationIdstringOperation id (UUID). Save it: it reads, resumes and cancels the operation.
pollUrlstringPath that reads the operation: GET it, with ?wait=20 to hold the request until it settles
totalRowsnumberLines queued after deduplication
duplicateCountnumberLines dropped as duplicates of an earlier line
sourceRowsnumberWith sourceUrl only: the non-empty data rows read from the file
mappingobjectWith sourceUrl only: the header each Signal field was read from, for example { "linkedinUrl": "Profile", "email": "Work email" }
jobobjectThe internal record behind the operation, left out of the example below. Its fields may change: read the operation at pollUrl instead.

quotas and metadata are the blocks of every V2 response: see Usage. quotas.creditsConsumed is 0: the credits of the operation appear in its creditsConsumed as it runs.

Then read the operation at its pollUrl until clientAction is done: see Read a List operation.

Webhook callback

With webhookUrl, that URL receives one POST each time the operation stops moving: it completed, paused at a ceiling or a low balance, was cancelled, or its outcome became unknown. It receives nothing while the operation runs. The event says which operation changed and carries no member data: read the operation, and its per-line results, at pollUrl 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 list.operation.updated
operationIdstringThe operation that changed
listIdstringIts List
statusstringStatus of the operation, as Read a List operation gives it
clientActionstringdone, resume or reconcile
stopReasonstring | nullWhy it stopped, null once completed
pollUrlstringPath that reads the operation
occurredAtstringWhen the event was sent (ISO 8601)

The request is JSON and its x-reversecontact-event header names the event, list.operation.updated. Answer with a 2xx within 10 seconds. A timeout, a 408, a 429 or a 5xx is tried again twice, after 2 then 5 seconds; a redirect is never followed. Each state is sent once, so a receiver that missed it can always read the operation at pollUrl.

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

Errors

StatusCodeMeaning
400LIST_INVALID_INPUTNeither or both of signals and sourceUrl, maxCredits missing above 25 credits, a family the List cannot hold, or an operationId sent again with another request
400WEBHOOK_URL_UNREACHABLEwebhookUrl is not a public HTTPS URL
403INSUFFICIENT_PERMISSIONSThe key lacks lists, or the permission of what the import buys
404LIST_NOT_FOUNDNo List with this id in your workspace
404LISTS_NOT_ACTIVATEDThe workspace has never had a List: create one first
422BULK_JOB_CSV_PARSE_ERRORThe sourceUrl file could not be read: not a public HTTPS URL, no header row, no recognized column, too many rows, or over 25 MiB
502LIST_JOB_ENGINE_ERRORThe List service is briefly unavailable: send the same request again, with the same operationId
503BULK_QUEUE_UNAVAILABLEThe import queue is busy: retry shortly
504GATEWAY_TIMEOUTThe request ran out of time after 15 seconds: send it again with the same operationId

Code examples

Code Examples

curl -X POST https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/intake/signals \
  -H "Authorization: Bearer YOUR_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" }
    ],
    "families": ["contact_email"],
    "maxCredits": 100,
    "webhookUrl": "https://example.com/hooks/reversecontact"
  }'

Example response

Example Response

{
  "success": true,
  "data": {
    "operationId": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
    "pollUrl": "/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/operations/c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f/progress",
    "totalRows": 2,
    "duplicateCount": 0
  },
  "error": null,
  "quotas": {
    "creditsConsumed": 0,
    "workspace": {
      "id": "ws_abc123",
      "credits": { "total": 10000, "used": 4522, "left": 5478 }
    }
  },
  "metadata": {
    "requestId": "req_i1n2t3a4k5e6",
    "executionTimeMs": 88
  }
}