# List the contacts of a campaign

Source: https://docs.emelia.io/docs/campaigns/list-contacts-of-campaign

Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md)

`GET https://api.emelia.io/advanced/campaign/contacts`

Paginated contacts of a campaign with their status in the campaign. `page` is ZERO based. `perPage` below 10 is replaced by 20; maximum 10,000.

## Authentication

Send your API key in the `Authorization` header: `Authorization: <your API key>`.

## Query parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Campaign id. |
| `page` | integer | yes | Zero based page index. Range: 0 to .... |
| `perPage` | integer | no | Default: `20`. Range: 10 to 10000. |
| `event` | string or array of string | no | Filter on contact status (for example `SENT`, `MAIL_REPLIED`; `NO_ACTIVITY` = no activity yet). Send it as an array: `event[]=SENT` or repeat the parameter. A single plain `event=SENT` is accepted by validation but currently fails with a 400 error. |
| `query` | string | no | Exact email, or words matched against email, names, phone and LinkedIn URL. |

## Example request

**cURL**

```bash
curl -X GET "https://api.emelia.io/advanced/campaign/contacts?id=string&page=0" \
  -H "Authorization: $EMELIA_API_KEY"
```

**JavaScript**

```js
const response = await fetch('https://api.emelia.io/advanced/campaign/contacts?id=string&page=0', {
  method: 'GET',
  headers: { Authorization: process.env.EMELIA_API_KEY },
});
console.log(await response.json());
```

**Python**

```python
import os
import requests

response = requests.get(
    "https://api.emelia.io/advanced/campaign/contacts?id=string&page=0",
    headers={"Authorization": os.environ["EMELIA_API_KEY"]},
)
print(response.json())
```

## Responses

### 200: Success.

| Field | Type | Required | Description |
|---|---|---|---|
| `success` | any | no |  |
| `list` | array of object | no |  |
| `list[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. |
| `list[].email` | string | no |  |
| `list[].firstName` | string | no |  |
| `list[].lastName` | string | no |  |
| `list[].phone` | string | no |  |
| `list[].linkedinUrlProfile` | string | no |  |
| `list[].status` | string | no | Status of the contact in this campaign (last meaningful activity event, for example `SENT`, `MAIL_REPLIED`), or `"-"` when nothing happened yet. |
| `list[].lastContacted` | string (date-time) | no |  |
| `list[].lastReplied` | string (date-time) | no |  |
| `list[].integrationContactId` | string | no |  |
| `list[].sentiment` | string | no |  |
| `list[].custom` | object | no | Custom variables (legacy inline copy). |
| `count` | integer | no |  |
| `_id` | string | no | Campaign id. |

```json
{
  "success": true,
  "list": [
    {
      "_id": "6650f1a2b839d80011c5a0f1",
      "email": "claire.fontaine@nexora.io",
      "firstName": "Claire",
      "lastName": "Fontaine",
      "phone": "+33 6 12 34 56 78",
      "linkedinUrlProfile": "https://www.linkedin.com/in/claire-fontaine",
      "status": "SENT",
      "lastContacted": "2026-10-06T09:30:00.000Z",
      "lastReplied": "2026-10-06T09:30:00.000Z",
      "integrationContactId": "6650f1a2b839d800ba9811eb",
      "sentiment": "INTERESTED",
      "custom": {}
    }
  ],
  "count": 250,
  "_id": "6650f1a2b839d800dcab1309"
}
```

### 400: Invalid request or business error (the API also uses 400 for "not found").

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | string or object | yes | Human readable message. A few legacy routes return an empty object `{}` here instead of a string. |
| `code` | enum: `INVALID_REQUEST`, `INVALID_PARAMETERS`, `SUBSCRIPTION_REQUIRED`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_ALREADY_RUNNING`, `SCHEDULE_MISSING`, `STEPS_MISSING`, `NO_RECIPIENTS`, `TOO_MANY_RECIPIENTS`, `PLANNED_START_INVALID`, `PLANNED_START_IN_PAST`, `EMAIL_PROVIDER_MISSING`, `EMAIL_PROVIDER_DISCONNECTED`, `EMAIL_PROVIDER_EXPIRED`, `ONE_EMAIL_PROVIDER_EXPIRED`, `EMAIL_ADDRESS_DISCONNECTED`, `TRACKING_DOMAINS_REQUIRED`, `LINKEDIN_ACCOUNT_MISSING`, `LINKEDIN_COOKIE_EXPIRED`, `LINKEDIN_SLOT_EXPIRED`, `WHATSAPP_ACCOUNT_EXPIRED`, `WHATSAPP_SLOT_EXPIRED`, `NO_IDENTITY_SET`, `IDENTITY_FOREIGN_ACCOUNT`, `IDENTITY_EMAIL_PROVIDER_MISSING`, `LINKEDIN_ACCOUNT_AMBIGUOUS`, `WHATSAPP_ACCOUNT_AMBIGUOUS`, `LINKEDIN_AUDIO_MISSING`, `EMAIL_STEP_NO_ACTIVE_VERSION`, `EMAIL_VERSION_NO_CONTENT`, `EMAIL_VERSION_NO_SUBJECT`, `RECIPIENTS_NO_EMAIL`, `RECIPIENTS_NO_LINKEDIN`, `MC_LINKEDIN_DAILY_LIMIT_CONNECTION`, `MC_LINKEDIN_DAILY_LIMIT`, `DAILY_CONTACT_TOO_LOW`, `LINKEDIN_DAILY_CONTACT_LIMIT`, `VERIFY_EMAIL_TOKEN_INVALID`, `VERIFY_EMAIL_TOKEN_EXPIRED`, `FREE_CREDITS_DOMAIN_ALREADY_CLAIMED`, `FREE_CREDITS_ALREADY_GRANTED`, `FREE_CREDITS_NOT_ELIGIBLE`, `RATE_LIMITED`, `STATS_COMPUTING`, `WARMUP_PROVIDER_UNSUPPORTED`, `ACCOUNT_DISCONNECTED` | no | Stable machine readable error code. Present only on catalogued errors (mainly the campaign start checks), on validation errors (`INVALID_REQUEST`) and on some LinkedIn provider errors (`ACCOUNT_DISCONNECTED`, `RATE_LIMITED`). Most other errors have no code: rely on the HTTP status and on `error`. Codes are never renamed once deployed. |
| `params` | object | no | Values interpolated in the message, for example `{"max": 30000}`. |

```json
{
  "error": "Campaign not found"
}
```

### 401: Missing, malformed, unknown or disabled API key. The key must be exactly 48 characters, otherwise it is treated as a session token and rejected the same way.

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | string or object | yes | Human readable message. A few legacy routes return an empty object `{}` here instead of a string. |
| `code` | enum: `INVALID_REQUEST`, `INVALID_PARAMETERS`, `SUBSCRIPTION_REQUIRED`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_ALREADY_RUNNING`, `SCHEDULE_MISSING`, `STEPS_MISSING`, `NO_RECIPIENTS`, `TOO_MANY_RECIPIENTS`, `PLANNED_START_INVALID`, `PLANNED_START_IN_PAST`, `EMAIL_PROVIDER_MISSING`, `EMAIL_PROVIDER_DISCONNECTED`, `EMAIL_PROVIDER_EXPIRED`, `ONE_EMAIL_PROVIDER_EXPIRED`, `EMAIL_ADDRESS_DISCONNECTED`, `TRACKING_DOMAINS_REQUIRED`, `LINKEDIN_ACCOUNT_MISSING`, `LINKEDIN_COOKIE_EXPIRED`, `LINKEDIN_SLOT_EXPIRED`, `WHATSAPP_ACCOUNT_EXPIRED`, `WHATSAPP_SLOT_EXPIRED`, `NO_IDENTITY_SET`, `IDENTITY_FOREIGN_ACCOUNT`, `IDENTITY_EMAIL_PROVIDER_MISSING`, `LINKEDIN_ACCOUNT_AMBIGUOUS`, `WHATSAPP_ACCOUNT_AMBIGUOUS`, `LINKEDIN_AUDIO_MISSING`, `EMAIL_STEP_NO_ACTIVE_VERSION`, `EMAIL_VERSION_NO_CONTENT`, `EMAIL_VERSION_NO_SUBJECT`, `RECIPIENTS_NO_EMAIL`, `RECIPIENTS_NO_LINKEDIN`, `MC_LINKEDIN_DAILY_LIMIT_CONNECTION`, `MC_LINKEDIN_DAILY_LIMIT`, `DAILY_CONTACT_TOO_LOW`, `LINKEDIN_DAILY_CONTACT_LIMIT`, `VERIFY_EMAIL_TOKEN_INVALID`, `VERIFY_EMAIL_TOKEN_EXPIRED`, `FREE_CREDITS_DOMAIN_ALREADY_CLAIMED`, `FREE_CREDITS_ALREADY_GRANTED`, `FREE_CREDITS_NOT_ELIGIBLE`, `RATE_LIMITED`, `STATS_COMPUTING`, `WARMUP_PROVIDER_UNSUPPORTED`, `ACCOUNT_DISCONNECTED` | no | Stable machine readable error code. Present only on catalogued errors (mainly the campaign start checks), on validation errors (`INVALID_REQUEST`) and on some LinkedIn provider errors (`ACCOUNT_DISCONNECTED`, `RATE_LIMITED`). Most other errors have no code: rely on the HTTP status and on `error`. Codes are never renamed once deployed. |
| `params` | object | no | Values interpolated in the message, for example `{"max": 30000}`. |

```json
{
  "error": "Authentication required"
}
```

### 500: Unexpected error. IMPORTANT: this is also what you receive today when the per minute quota of your account is exceeded (the quota error is not mapped to 429 for API keys). No `Retry-After` header is sent. Wait until the next minute window before retrying.

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | string or object | yes | Human readable message. A few legacy routes return an empty object `{}` here instead of a string. |
| `code` | enum: `INVALID_REQUEST`, `INVALID_PARAMETERS`, `SUBSCRIPTION_REQUIRED`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_ALREADY_RUNNING`, `SCHEDULE_MISSING`, `STEPS_MISSING`, `NO_RECIPIENTS`, `TOO_MANY_RECIPIENTS`, `PLANNED_START_INVALID`, `PLANNED_START_IN_PAST`, `EMAIL_PROVIDER_MISSING`, `EMAIL_PROVIDER_DISCONNECTED`, `EMAIL_PROVIDER_EXPIRED`, `ONE_EMAIL_PROVIDER_EXPIRED`, `EMAIL_ADDRESS_DISCONNECTED`, `TRACKING_DOMAINS_REQUIRED`, `LINKEDIN_ACCOUNT_MISSING`, `LINKEDIN_COOKIE_EXPIRED`, `LINKEDIN_SLOT_EXPIRED`, `WHATSAPP_ACCOUNT_EXPIRED`, `WHATSAPP_SLOT_EXPIRED`, `NO_IDENTITY_SET`, `IDENTITY_FOREIGN_ACCOUNT`, `IDENTITY_EMAIL_PROVIDER_MISSING`, `LINKEDIN_ACCOUNT_AMBIGUOUS`, `WHATSAPP_ACCOUNT_AMBIGUOUS`, `LINKEDIN_AUDIO_MISSING`, `EMAIL_STEP_NO_ACTIVE_VERSION`, `EMAIL_VERSION_NO_CONTENT`, `EMAIL_VERSION_NO_SUBJECT`, `RECIPIENTS_NO_EMAIL`, `RECIPIENTS_NO_LINKEDIN`, `MC_LINKEDIN_DAILY_LIMIT_CONNECTION`, `MC_LINKEDIN_DAILY_LIMIT`, `DAILY_CONTACT_TOO_LOW`, `LINKEDIN_DAILY_CONTACT_LIMIT`, `VERIFY_EMAIL_TOKEN_INVALID`, `VERIFY_EMAIL_TOKEN_EXPIRED`, `FREE_CREDITS_DOMAIN_ALREADY_CLAIMED`, `FREE_CREDITS_ALREADY_GRANTED`, `FREE_CREDITS_NOT_ELIGIBLE`, `RATE_LIMITED`, `STATS_COMPUTING`, `WARMUP_PROVIDER_UNSUPPORTED`, `ACCOUNT_DISCONNECTED` | no | Stable machine readable error code. Present only on catalogued errors (mainly the campaign start checks), on validation errors (`INVALID_REQUEST`) and on some LinkedIn provider errors (`ACCOUNT_DISCONNECTED`, `RATE_LIMITED`). Most other errors have no code: rely on the HTTP status and on `error`. Codes are never renamed once deployed. |
| `params` | object | no | Values interpolated in the message, for example `{"max": 30000}`. |

```json
{
  "error": "Internal server error"
}
```
