> Reverse Contact API documentation. This is the Markdown copy of https://docs.reversecontact.com/endpoints/list-members-add
> Every page is listed at https://docs.reversecontact.com/llms.txt, every endpoint with its credit cost and status at https://docs.reversecontact.com/catalog.json

# Add members

> Add up to 1,000 people or companies to a List from Social URLs, emails, names or domains, with one result per Signal.

- Endpoint: `POST https://api.reversecontact.com/v2/lists/:listId/members`
- 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.
- Answer: synchronous.
- Available in: REST API, MCP tool `add_list_members`, TypeScript and Python SDKs, CLI command `rc lists add`.
- Last verified: 2026-11-04

## 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](https://docs.reversecontact.com/guides/lists#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](https://docs.reversecontact.com/endpoints/list-intake-signals) 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](https://docs.reversecontact.com/guides/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](https://docs.reversecontact.com/endpoints/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](https://docs.reversecontact.com/endpoints/list-intake-signals).

| Field     | Type    | Description                                      |
| --------- | ------- | ------------------------------------------------ |
| `success` | boolean | `true` if the request was processed              |
| `error`   | `null`  | Always `null` on success                         |

**data** (AddMembers): What became of the Signals

| 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) |

**results** (SignalResult[]): One entry per Signal, in request order

| 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](https://docs.reversecontact.com/endpoints/usage#response-structure).

## 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**

```bash [cURL]
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" }
    ]
  }'
```
```javascript [JavaScript]
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);
```
```python [Python]
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**

```json [200 - Success]
{
  "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": "73e35798-7fd6-4c5f-8545-5f351b795b76",
    "executionTimeMs": 2140
  }
}
```
