Endpoints // Lists
Add up to 1,000 people or companies to a List from Social URLs, emails, names or domains, with one result per Signal.
Credits
Free to call.
Each new person costs 1 credit from a Social profile URL (2 with live), 2 from an email or a name. Each new company costs 1 credit 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, or a Signal no one matches, is free.
Introduction
The Add members endpoint turns up to 1,000 Signals into List Members and answers once they are all done. A Signal is what you know about someone: a Social profile URL, an email, or a first and last name with a company for a person; a company page URL, a domain or a company id for a company. The response holds one result per Signal, in the order you sent them, so you can match each line of your file to the member it became.
A Signal no one matches is kept on the List as Pending Intake, free. When a Signal sent again finds someone, the new member replaces its Pending Intake line. A person or company already on the List is left as it is, free, with one exception: the email of an email Signal becomes the member's work email (families.contactEmail, source: "signal:paste" or "signal:file") when the List has not found one yet, for free. This holds for a new member too, so a later email lookup never buys back an address you gave.
To add more than 1,000 Signals, to buy data families (profile, company, email, phone) for each member, to set a credit ceiling or to be told by webhook, use Import members and data instead.
Credits
The call itself is free: only the new members are charged, at the price of the Signal that found them.
| Signal | People List | Companies List |
|---|---|---|
| Social URL, from saved data | 1 credit | 1 credit, 2 when the company page is read live |
Social URL with live: true | 2 credits | As without live: live has no effect on companies |
| Email, or first and last name with a company | 2 credits | Refused |
Company id (com_...) | Refused | 1 credit, 2 when the company page is read live |
| Domain alone | Refused | 2 credits |
| Already on the List, or matched by no one | Free | Free |
A Signal of the other kind (a company on a people List, a person on a companies List) refuses the request with 400 LIST_ENTITY_MISMATCH. Before anything is added, the API checks that your balance covers what the batch could charge: every Signal at its price, except a Social URL already on the List or repeated in the batch (an email, a name, a domain or a company id is always counted, and a company page URL or id at 1 credit). Below that, the request returns 402 and nothing is charged. Each new member is then charged before it is written. A person or company that asked us to stop processing their data is refused (data_subject_blocked): nothing is kept or charged for it. data.creditsConsumed and quotas.creditsConsumed both give what the call charged.
The API key needs the lists permission, plus persons:enrich for a people List or companies:enrich for a companies List (see API key management).
Authorization
| Name | Type | Required | Description |
|---|---|---|---|
apikey | string | Yes | Your 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/members.
| Name | Type | Required | Description |
|---|---|---|---|
signals | array | Yes | 1 to 1,000 Signals, described below |
live | boolean | No | true fetches the profile of a Social profile URL live (2 credits). Omitted or false, saved data is used (1 credit). No effect on a companies List. |
source | string | No | paste (default) or file: how a Pending Intake line says it arrived |
Each Signal is an object with the fields that identify one person or one company:
| Field | Type | Description |
|---|---|---|
linkedinUrl | string | Profile URL (people List) or company page URL (companies List). url is accepted as an alias. |
email | string | Email of the person |
firstName | string | First name, with lastName and companyDomain or companyName |
lastName | string | Last name, with firstName and companyDomain or companyName |
companyDomain | string | For a person, the domain of their company. On a companies List, the company itself. |
companyName | string | Company name, a hint for a person found by name |
domain | string | Company domain, on a companies List |
companyId | string | Company id (com_...) returned by our API, on a companies List |
externalId | string | Your own identifier for the line, 1 to 200 characters. It is echoed in the line result and kept on the member, so List members can find it. A member keeps the first one it receives. It is never used to find anyone. |
One invalid Signal refuses the whole request before anything is charged, and error.details.signalIndex gives its position.
Response structure
The request answers once every Signal has been handled. It runs within a 62-second time limit: past it, it returns 504, and the members already added stay added and charged. Read the List to see where it stopped, or send large batches through the import.
| Field | Type | Description |
|---|---|---|
success | boolean | true if the request was processed |
error | null | Always null on success |
dataobjectWhat became of the Signals
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
inserted | number | Members added |
skipped | number | Signals naming a member already on the List |
refused | number | Signals refused for the member limit, an ambiguous identity or data_subject_blocked. A line stopped by credits or by the Pending Intake limit reads refused in results but is not counted here. |
unresolvedCount | number | Signals no one matched, kept as Pending Intake |
creditsConsumed | number | Credits charged by this call |
stoppedReason | string | Present when a stop left later Signals unread: quota (credits), member_cap or pending_cap (List limits) |
resultsobject[]One entry per Signal, in request order
Show child attributesHide child attributes
| Field | Type | Description |
|---|---|---|
index | number | Position of the Signal in the request, from 0 |
outcome | string | added, already_present, not_found, refused, failed or not_reached |
externalId | string | Your identifier, as sent |
memberId | string | The member the line names, for added and already_present |
entityId | string | Person (prs_...) or company (com_...) id |
pendingId | string | The Pending Intake line kept for a not_found Signal |
reason | string | Why a line was refused, failed or not reached: member_limit, pending_intake_limit, insufficient_credits, ambiguous_identity, data_subject_blocked, identity_unavailable, quota_unavailable or stopped |
quotas and metadata are the blocks of every V2 response: see Usage.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | LIST_INVALID_INPUT | A Signal cannot identify anyone, see error.details.signalIndex |
400 | LIST_ENTITY_MISMATCH | A Signal names a company on a people List, or a person on a companies List |
402 | INSUFFICIENT_CREDITS | The balance cannot pay for what the batch could charge |
403 | INSUFFICIENT_PERMISSIONS | The key lacks lists, or the enrich permission of the List's entity |
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 |
409 | LIST_MEMBER_LIMIT_REACHED | A request with a single Signal, on a List that already holds 25,000 members (50 on the free trial) |
409 | LIST_PENDING_INTAKE_LIMIT_REACHED | A request with a single Signal no one matches, on a List that already holds 25,000 Pending Intake lines (50 on the free trial) |
504 | GATEWAY_TIMEOUT | The request ran out of time after 62 seconds: the Signals already added stay added |
With several Signals, a full List does not refuse the request: it answers 200, the line that met the limit reads refused (member_limit or pending_intake_limit), the next ones read not_reached, and stoppedReason is member_cap or pending_cap.
Code examples
Code Examples
curl -X POST https://api.reversecontact.com/v2/lists/3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40/members \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"signals": [
{ "linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001" },
{ "email": "john.smith@acme.com", "externalId": "crm-002" },
{ "firstName": "Ana", "lastName": "Lopez", "companyDomain": "northwind.com", "externalId": "crm-003" }
]
}'const listId = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40";
const response = await fetch(`https://api.reversecontact.com/v2/lists/${listId}/members`, {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
signals: [
{ linkedinUrl: "https://www.social.com/in/janedoe", externalId: "crm-001" },
{ email: "john.smith@acme.com", externalId: "crm-002" },
{ firstName: "Ana", lastName: "Lopez", companyDomain: "northwind.com", externalId: "crm-003" }
]
})
});
const { data } = await response.json();
for (const result of data.results) {
console.log(result.externalId, result.outcome, result.memberId ?? result.pendingId);
}
console.log("Credits charged:", data.creditsConsumed);import requests
list_id = "3f6c2a8e-1b4d-4f0a-9c7e-5d2b8a1e6f40"
response = requests.post(
f"https://api.reversecontact.com/v2/lists/{list_id}/members",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
json={
"signals": [
{"linkedinUrl": "https://www.social.com/in/janedoe", "externalId": "crm-001"},
{"email": "john.smith@acme.com", "externalId": "crm-002"},
{"firstName": "Ana", "lastName": "Lopez", "companyDomain": "northwind.com", "externalId": "crm-003"}
]
}
)
data = response.json()["data"]
for result in data["results"]:
print(result.get("externalId"), result["outcome"])
print("Credits charged:", data["creditsConsumed"])Example response
In this example, the Social profile URL adds a new member (1 credit), the email names a person already on the List (free), and no one matches the name (free, kept as Pending Intake).
Example Response
{
"success": true,
"data": {
"inserted": 1,
"skipped": 1,
"refused": 0,
"unresolvedCount": 1,
"creditsConsumed": 1,
"results": [
{
"index": 0,
"outcome": "added",
"externalId": "crm-001",
"memberId": "9b1e4c7a-2d3f-4e8a-b6c5-0a1f2e3d4c5b",
"entityId": "prs_01jbd5vewyenebxgaz869ffe5t"
},
{
"index": 1,
"outcome": "already_present",
"externalId": "crm-002",
"memberId": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
"entityId": "prs_01h2x3y4z5a6b7c8d9e0f1g2h3"
},
{
"index": 2,
"outcome": "not_found",
"externalId": "crm-003",
"pendingId": "7e8f9a0b-1c2d-4e3f-8a4b-5c6d7e8f9a0b"
}
]
},
"error": null,
"quotas": {
"creditsConsumed": 1,
"workspace": {
"id": "ws_abc123",
"credits": { "total": 10000, "used": 4522, "left": 5478 }
}
},
"metadata": {
"requestId": "req_a1d2d3m4e5m6",
"executionTimeMs": 2140
}
}