# Emelia developer documentation (full text) > Every page of https://docs.emelia.io, as Markdown, in the order of the sidebar. Index: https://docs.emelia.io/llms.txt - REST API base URL: https://api.emelia.io (lists and contacts are served under https://api.emelia.io/lists, same key) - Authentication: send your 48 character API key in the `Authorization` header, as is (a `Bearer ` prefix is accepted). Create the key at https://v3.emelia.io/settings/api. A key gives access to the whole account. - Rate limit per account and per minute: 30 without a plan, 100 Start, 300 Grow, 1,000 Scale. `/tools` and `/lists` routes are not counted. Over the limit the API answers HTTP 500 (not 429): wait for the next minute. - Errors: `{ "error", "code"?, "params"? }`; "not found" is HTTP 400 on the main API. The lists service answers `{ "message", "code"? }` with real 404, 413 and 422. - Credits: Email Finder 1 (refunded when nothing is found), Phone Finder 50 (refunded when nothing is found), Email Verifier 0.25 (never refunded). - OpenAPI specs: https://docs.emelia.io/openapi/emelia.json - MCP server: https://mcp.emelia.io/mcp (Streamable HTTP, same API key in the `Authorization` header, raw or `Bearer`, or in `X-Api-Key`) - Every page is available as Markdown: append `.md` to its URL. All pages in one file: https://docs.emelia.io/llms-full.txt - Step and version `_id` values of a campaign sequence are MongoDB ObjectIds (24 lowercase hex characters), never UUIDs. The simplest is to omit them: the server generates them. # Introduction > Automate your prospecting with the Emelia API. Campaigns, contact lists, Email Finder, Phone Finder, Email Verifier, replies and webhooks, from your code or your AI assistant. Source: https://docs.emelia.io/docs Emelia is a B2B prospecting platform: multichannel campaigns (cold email, LinkedIn, WhatsApp), contact lists, an Email Finder, a Phone Finder, an Email Verifier, mailbox warmup and a unified inbox. The REST API gives your code the same power as the app. [Get your API key](https://v3.emelia.io/settings/api) [Open Emelia](https://v3.emelia.io) ## What you can build - [Feed campaigns from your stack](https://docs.emelia.io/docs/lists): Push contacts from your CRM, a form or a Make or n8n flow. They enter the campaigns of their list on their own. - [Create campaigns by API](https://docs.emelia.io/docs/campaigns): Build the sequence, the schedule and the senders, then start it. Cold email, LinkedIn automation, or both. - [Enrich contacts](https://docs.emelia.io/docs/enrichment): Find professional emails and phone numbers, verify addresses before you send. - [Work the replies](https://docs.emelia.io/docs/replies): Read every reply with its AI classification, answer from the right mailbox, sync your CRM. ## How Emelia is organised Three ideas explain most of the API. 1. **Contacts live in lists, not in campaigns.** A campaign reads one or several lists. Add a contact to a list used by a campaign and the contact enters the campaign, without touching the campaign. 2. **Every campaign is multichannel.** The steps of a campaign decide what it does: emails only, LinkedIn actions only, or both, plus WhatsApp and manual tasks. There is one way to create a campaign, whatever the channels. 3. **Enrichment is asynchronous.** Finding an email or a phone number starts a job. You get a `jobId` at once and read the result when the job is done. ## The APIs | | Base URL | What it covers | Key | | --------------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------- | | [Emelia API](https://docs.emelia.io/docs/all-endpoints.md) | `https://api.emelia.io` | Campaigns, lists and contacts, enrichment, replies, blacklist, webhooks, email providers, LinkedIn scraper | Your Emelia API key | | [MCP server](https://docs.emelia.io/docs/ai/mcp.md) | `https://mcp.emelia.io/mcp` | 26 tools for Claude, Cursor, VS Code and other AI assistants | The same key | ## Start here - [Quickstart](https://docs.emelia.io/docs/quickstart): Your first request in two minutes: a key, a list, a contact. - [Authentication](https://docs.emelia.io/docs/authentication): Create a key, send it, keep it safe. - [Use Emelia from Claude or Cursor](https://docs.emelia.io/docs/ai/mcp): Connect the Emelia MCP server and drive your account in plain language. - [All endpoints](https://docs.emelia.io/docs/all-endpoints): Every route on one page, and the OpenAPI specs to download. > **Need a hand?.** Write to [contact@emelia.io](mailto:contact@emelia.io), or use the chat in the [Emelia app](https://v3.emelia.io). --- # Quickstart > Make your first Emelia API call in two minutes, then create a list and add a contact to it. Source: https://docs.emelia.io/docs/quickstart By the end of this page you will have an API key, a working request, a list, and a contact in that list. ### Get your API key Open **Settings, API & Webhooks** in the Emelia app and create a key. [Get your API key](https://v3.emelia.io/settings/api) Store it in an environment variable rather than in your code: ```bash export EMELIA_API_KEY="your_48_character_api_key" ``` ### Check that the key works List your campaigns. This call changes nothing. [`GET /advanced/campaigns`](https://docs.emelia.io/docs/campaigns/list-campaigns.md) **curl** ```bash curl https://api.emelia.io/advanced/campaigns \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/advanced/campaigns', { headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os import requests res = requests.get( "https://api.emelia.io/advanced/campaigns", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(res.status_code, res.json()) ``` A `200` with a `campaigns` array means you are in. A `401` means the key is wrong or disabled: see [Authentication](https://docs.emelia.io/docs/authentication.md). ### Create a list Contacts live in lists. Create one: [`POST /lists/list`](https://docs.emelia.io/docs/lists/create-list.md) **curl** ```bash curl -X POST https://api.emelia.io/lists/list \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Q4 SaaS founders"}' ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/lists/list', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'Q4 SaaS founders' }), }); const { listId } = await res.json(); ``` **Python** ```python res = requests.post( "https://api.emelia.io/lists/list", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={"name": "Q4 SaaS founders"}, ) list_id = res.json()["listId"] ``` The response is a `201`: ```json { "success": true, "listId": "66f1c0ffee5a7d3b2c4e8a10", "name": "Q4 SaaS founders" } ``` ### Add a contact Send a flat object: one key per field. Emelia recognises the standard fields (`firstName`, `email`, `companyName`...) and turns any other key into a custom variable you can use in your messages. [`POST /lists/list/{listId}/contacts`](https://docs.emelia.io/docs/lists/add-contact.md) **curl** ```bash curl -X POST https://api.emelia.io/lists/list/66f1c0ffee5a7d3b2c4e8a10/contacts \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Claire", "lastName": "Fontaine", "email": "claire.fontaine@nexora.io", "companyName": "Nexora", "icebreaker": "your move to Postgres 16 last month" }' ``` **JavaScript** ```js const listId = '66f1c0ffee5a7d3b2c4e8a10'; const res = await fetch(`https://api.emelia.io/lists/list/${listId}/contacts`, { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ firstName: 'Claire', lastName: 'Fontaine', email: 'claire.fontaine@nexora.io', companyName: 'Nexora', icebreaker: 'your move to Postgres 16 last month', }), }); console.log(await res.json()); ``` **Python** ```python list_id = "66f1c0ffee5a7d3b2c4e8a10" res = requests.post( f"https://api.emelia.io/lists/list/{list_id}/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "firstName": "Claire", "lastName": "Fontaine", "email": "claire.fontaine@nexora.io", "companyName": "Nexora", "icebreaker": "your move to Postgres 16 last month", }, ) print(res.json()) ``` ```json title="Response" { "success": true, "leadId": "66f2a1b2c3d4e5f6a7b8c9d0", "companyId": "66f2a1b2c3d4e5f6a7b8c9e0", "duplicate": false, "updated": false, "createdCustomVariables": [{ "technicalName": "icebreaker", "displayName": "icebreaker", "schema": "lead" }] } ``` Send the same contact again and nothing is created: the response carries the existing `leadId` with `duplicate: true`. The match is on the exact `email`, then on the exact LinkedIn URL. A contact with neither is created again on every call, so give each contact an email or a LinkedIn URL before you retry. ### Attach the list to a campaign In the app, open a campaign and select the list as its recipients, or do it by API: [`PATCH /advanced/campaigns/{campaignId}/recipients`](https://docs.emelia.io/docs/campaigns/set-contact-lists.md) From then on, every contact you add to the list enters the campaign on its own, even while it runs. ## Next steps - [Lists & contacts](https://docs.emelia.io/docs/lists): Batches of 100 contacts, updates, lookups, field mapping. - [Build a campaign by API](https://docs.emelia.io/docs/campaigns): Sequence, schedule, senders, start. - [Skip the code](https://docs.emelia.io/docs/ai/mcp): Ask Claude or Cursor to do the same through the MCP server. --- # Authentication > Create an Emelia API key in Settings, API & Webhooks, and send it in the Authorization header of every request. Source: https://docs.emelia.io/docs/authentication Every request to the Emelia API carries your API key in the `Authorization` header. There is no OAuth flow and no token to refresh. ## Create a key [Get your API key](https://v3.emelia.io/settings/api) 1. Open the Emelia app and go to **Settings, API & Webhooks** ([v3.emelia.io/settings/api](https://v3.emelia.io/settings/api)). 2. Click **Create new key**. 3. Copy the key and store it as a secret, for example in an `EMELIA_API_KEY` environment variable. A key is exactly 48 letters and digits. You can create several keys, and disable or delete one at any time from the same page. > **A key opens the whole account.** There are no scopes: a key can read and change your campaigns, lists and contacts, spend your credits and act on every part of your account. Protect it like a password, and give one key to each integration. ## Send the key Put the key, as is, in the `Authorization` header: [`GET /advanced/campaigns`](https://docs.emelia.io/docs/campaigns/list-campaigns.md) **curl** ```bash curl https://api.emelia.io/advanced/campaigns \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const emelia = (path, init = {}) => fetch(`https://api.emelia.io${path}`, { ...init, headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json', ...init.headers, }, }); const res = await emelia('/advanced/campaigns'); console.log(await res.json()); ``` **Python** ```python import os import requests session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] res = session.get("https://api.emelia.io/advanced/campaigns") print(res.json()) ``` A `Bearer ` prefix is accepted too (`Authorization: Bearer `), which helps with HTTP clients that add it on their own. The same key works for the whole host, including the lists and contacts routes under `https://api.emelia.io/lists`, and for the [MCP server](https://docs.emelia.io/docs/ai/mcp.md). ## When the key is refused The API answers `401`: ```json { "error": "Authentication required" } ``` The lists and contacts routes (`/lists/...`) add a code: `{ "error": "Authentication required", "code": "AUTH_REQUIRED" }`. Check, in this order: 1. **The variable is empty** in the process that sends the request. `echo ${#EMELIA_API_KEY}` prints its length (48), without showing the key. 2. **The key has extra characters**: a space, a line break, quotes. A value that is not exactly 48 characters long is never read as an API key. 3. **The key was disabled or deleted** in Settings, API & Webhooks. A disabled key gets the same `401` as an unknown one. 4. **It is another product's key**, or an Emelia Inbox API key (`mk_...`): it gets a plain `401` here. ## Keep it safe * Always send the key in the `Authorization` header. Never put it in a URL, a query string or front end code. * Call Emelia from your server, never from a browser or a mobile app. * One key per integration (Make, n8n, your back end, an AI agent): if one leaks, you disable it without breaking the others. * If a key leaked, delete it in Settings, API & Webhooks, then create a new one and update your integrations. --- # Base URLs > Where to send each request, the request format, the body size limits and the identifiers used by the Emelia API. Source: https://docs.emelia.io/docs/base-urls All requests go over HTTPS, with JSON bodies. | Use | Base URL | Example | | -------------------------------------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------- | | Emelia API: campaigns, enrichment, replies, blacklist, webhooks, email providers, LinkedIn scraper | `https://api.emelia.io` | `GET https://api.emelia.io/advanced/campaigns` | | Lists and contacts | `https://api.emelia.io/lists` | `POST https://api.emelia.io/lists/list` | | MCP server, for AI assistants | `https://mcp.emelia.io/mcp` | See [MCP server](https://docs.emelia.io/docs/ai/mcp.md) | The paths in this documentation are complete: `/lists/list` means `https://api.emelia.io/lists/list`. There is no version prefix in the paths. ## One host, two services The lists and contacts routes (`/lists/...`) are served by a separate service behind the same host. You use the same key, but a few rules differ, and each page says so: | | Main API | Lists and contacts (`/lists/...`) | | ---------------- | ----------------------------------------- | --------------------------------- | | Error body | `{ "error", "code"?, "params"? }` | `{ "message", "code"? }` | | "Not found" | HTTP 400 | HTTP 404 | | Body size limit | 30 MB | 5 MB, then `413` | | Per minute quota | Yes, see [Rate limits](https://docs.emelia.io/docs/rate-limits.md) | Not counted | ## Current and legacy campaign routes Campaigns are served under `/advanced/...`. This is the current campaign system, the one of the Emelia app: every campaign is multichannel. Use it for all new work, whatever the channels. The `/emails/...` and `/linkedin/...` campaign routes belong to the previous system, where a campaign was either email only or LinkedIn only. They keep working for existing integrations and are documented under [Campaigns, Legacy](https://docs.emelia.io/docs/campaigns/legacy.md). The [blacklist](https://docs.emelia.io/docs/blacklist.md) routes also start with `/emails/` for historical reasons: they are current and apply to every campaign. ## Request format * Bodies are JSON: send `Content-Type: application/json`. File uploads use `multipart/form-data` and say so on their page. * Responses are JSON, except file downloads (CSV). * Dates are ISO 8601 strings in UTC, for example `2026-10-15T08:00:00.000Z`. * Emelia ids (campaigns, lists, contacts, steps, jobs) are MongoDB ObjectIds: 24 lowercase hexadecimal characters, for example `6650f1a2b839d80011c5a0f1`. The export jobs of lists are the exception: their `jobId` is a UUID. * A request that runs longer than 120 seconds is cut by the load balancer. Long work (enrichment, exports, extractions) runs as background jobs instead. * An unknown path answers an HTML 404 page, not JSON. If you get HTML back, check the path and the method. ## Test environment There is no separate test environment: every request acts on your real account. To experiment, create a list and a draft campaign named "Test", and delete them afterwards. A draft campaign never sends anything until you start it. --- # Rate limits & quotas > How many requests you can send per minute on each plan, what happens over the limit, the size limits of each call, and how to pace a large job. Source: https://docs.emelia.io/docs/rate-limits ## Requests per minute The Emelia API counts the requests of your account over a 60 second window. All the API keys of an account share the same counter. | Plan | Requests per minute | | -------------- | ------------------- | | No active plan | 30 | | Start | 100 | | Grow | 300 | | Scale | 1,000 | The window is fixed, not sliding: it starts with your first request and lasts 60 seconds, then a new window starts with the next request. What does not count: * The enrichment routes under `/tools/...` (Email Finder, Phone Finder, Email Verifier, and the polling of their results). * The lists and contacts routes under `/lists/...`. The [MCP server](https://docs.emelia.io/docs/ai/mcp.md) relays your key to this API: its tools count like direct calls, under the same rules. ## Over the limit: HTTP 500, not 429 Today, a request over the limit gets an HTTP `500` with this body, and no `Retry-After` header: ```json { "error": "Internal server error" } ``` That is the current behaviour, not the usual `429`. Here is how to handle it: * **Recognise it.** A `500` with exactly `{"error": "Internal server error"}` during a burst of requests is almost always the quota. * **Wait for the next window.** Pause for 60 seconds, then resume at a slower pace. Retrying at once only fails again. * **Retry safely.** Retry `GET` requests freely. Before retrying a `POST`, check that the first call did not go through (for example, look for the contact you were adding): the quota is checked before the request runs, but a real server error looks the same. [`GET /advanced/campaigns`](https://docs.emelia.io/docs/campaigns/list-campaigns.md) **curl** ```bash # Retry once after the window when the quota is hit status=$(curl -s -o response.json -w "%{http_code}" https://api.emelia.io/advanced/campaigns \ -H "Authorization: $EMELIA_API_KEY") if [ "$status" = "500" ]; then sleep 60 curl -s https://api.emelia.io/advanced/campaigns -H "Authorization: $EMELIA_API_KEY" else cat response.json fi ``` **JavaScript** ```js async function emelia(path, init = {}, attempt = 0) { const res = await fetch(`https://api.emelia.io${path}`, { ...init, headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json', ...init.headers }, }); // Over the per minute quota the API answers 500 today, not 429. if (res.status === 500 && attempt < 2 && (init.method ?? 'GET') === 'GET') { await new Promise((resolve) => setTimeout(resolve, 60_000)); return emelia(path, init, attempt + 1); } return res; } const res = await emelia('/advanced/campaigns'); console.log(await res.json()); ``` **Python** ```python import os import time import requests session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] def get(path, attempt=0): res = session.get(f"https://api.emelia.io{path}") # Over the per minute quota the API answers 500 today, not 429. if res.status_code == 500 and attempt < 2: time.sleep(60) return get(path, attempt + 1) return res print(get("/advanced/campaigns").json()) ``` ## Size limits | What | Limit | | ------------------------------------------------------------- | --------------------------------------------------- | | Request body, main API | 30 MB | | Request body, lists and contacts (`/lists/...`) | 5 MB, then `413` with `"code": "PAYLOAD_TOO_LARGE"` | | Contacts per batch call ([Add contacts in bulk](https://docs.emelia.io/docs/lists.md)) | 1 to 100, `422` otherwise | | Lists per page (`GET /lists/list`) | 25 by default, 100 at most | | Contacts per page (`GET /lists/list/{listId}/rows`) | 50 by default, 100 at most | | Exact lookup of a contact by email or LinkedIn URL | 50 results, no pagination | | Campaign attachments | 3 files of 20 MB | | LinkedIn voice note | 10 MB and 5 minutes | | Duration of a request | 120 seconds | ## Campaign quotas These limits belong to each campaign, in its settings ([Update campaign settings](https://docs.emelia.io/docs/campaigns/update-campaign-settings.md)): | Setting | Range | | ------------------------------------------------------------------------- | ----------------------------------------------------------- | | `dailyEmailAdded`, new contacts entering the email sequence per day | 1 to 500 | | `dailyEmailLimit`, emails sent per day, follow-ups included | 1 to 500, and at least `dailyEmailAdded` | | `dailyLinkedinAdded`, new contacts entering the LinkedIn sequence per day | 1 to 80, and 40 at most when the sequence sends invitations | | `dailyWhatsappAdded`, new contacts entering the WhatsApp sequence per day | 1 to 30 | A campaign starts with 30,000 recipients at most. Above it, the start fails with the error code `TOO_MANY_RECIPIENTS`. ## Pacing a large job * **Batch first.** Loading 1,000 contacts is 10 batch calls of 100, not 1,000 single calls. * **Spread the calls.** On Start, keep a steady rhythm under 100 requests per minute rather than bursts. * **Never retry an enrichment blindly.** A retried `POST /tools/find/email` starts a second search and is billed twice. Poll the `jobId` you already have. See [Enrichment](https://docs.emelia.io/docs/enrichment.md). * **On an error in the middle of a batch,** resume from the failed row instead of restarting: the rows already loaded come back as duplicates. --- # Pagination > Several pagination styles coexist in the Emelia API. Which route uses which, where pages start at 0 or at 1, and a loop for each. Source: https://docs.emelia.io/docs/pagination There is no single convention: check the route page. This table covers every paginated route. | Routes | First page | Page size | Response | | ----------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------- | -------------------------------------------------- | | [Lists](https://docs.emelia.io/docs/lists.md): `GET /lists/list` | `page=1` | `limit`, 25 by default, 100 at most | `items`, `total`, `page`, `pageSize`, `totalLeads` | | [Contacts of a list](https://docs.emelia.io/docs/lists.md): `GET /lists/list/{listId}/rows` | `page=1` | `pageSize`, 50 by default, 100 at most | `items`, `total`, `page`, `pageSize` | | [Contacts of a campaign](https://docs.emelia.io/docs/campaigns.md): `GET /advanced/campaign/contacts` | `page=0` | `perPage`, 20 by default, 10,000 at most | `list`, `count` | | [Campaign activities](https://docs.emelia.io/docs/replies.md): `GET /advanced/campaigns/{campaignId}/activities` | `page=0` | 30, fixed | `activities` | | Contacts of a step: `GET /advanced/campaigns/{campaignId}/progress/contacts` | `page=1` | 20, fixed | `contacts`, `total`, `totalPages` | | [Enrichment history](https://docs.emelia.io/docs/enrichment.md): `GET /tools/find/email`, `/tools/find/phone`, `/tools/verify/email` | `page=1` | `limit`, 15 by default, 100 at most | `data`, `total` | | [Credit history](https://docs.emelia.io/docs/credits.md): `GET /credits/usage` | `page=1` | `limit`, 50 by default, 200 at most | `items`, `total` | | [LinkedIn scrapers](https://docs.emelia.io/docs/linkedin-scraper.md): `GET /linkedin-scrappers` | `page=0` | 20, fixed | `scraps` | Not paginated: the campaigns (`GET /advanced/campaigns`), the email providers and the warmups. They return everything in one response. ## Lists and contacts Read every contact of a list, 100 at a time: [`GET /lists/list/{listId}/rows`](https://docs.emelia.io/docs/lists/list-or-find-contacts.md) **curl** ```bash curl "https://api.emelia.io/lists/list/66f1c0ffee5a7d3b2c4e8a10/rows?page=1&pageSize=100" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js async function* allContacts(listId) { for (let page = 1; ; page++) { const url = `https://api.emelia.io/lists/list/${listId}/rows?page=${page}&pageSize=100`; const res = await fetch(url, { headers: { Authorization: process.env.EMELIA_API_KEY } }); const { items, total } = await res.json(); yield* items; if (items.length === 0 || page * 100 >= total) return; } } for await (const contact of allContacts('66f1c0ffee5a7d3b2c4e8a10')) console.log(contact.email); ``` **Python** ```python import os import requests session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] def all_contacts(list_id): page = 1 while True: res = session.get( f"https://api.emelia.io/lists/list/{list_id}/rows", params={"page": page, "pageSize": 100}, ).json() yield from res["items"] if not res["items"] or page * 100 >= res["total"]: return page += 1 for contact in all_contacts("66f1c0ffee5a7d3b2c4e8a10"): print(contact.get("email")) ``` > **Two traps of the list routes.** `GET /lists/list` without `page` and `limit` returns a plain array of all your lists instead of the envelope: send both parameters to get a stable shape. And deep pages of a large list get slower, because each page sorts every row before it: page through with `pageSize=100` without restarting from page 1, or use the [CSV export](https://docs.emelia.io/docs/lists.md). ## Campaign activities `GET /advanced/campaigns/{campaignId}/activities` returns 30 activities per page, newest first, and `page` starts at **0**. An empty `activities` array means you reached the end. [`GET /advanced/campaigns/{campaignId}/activities`](https://docs.emelia.io/docs/replies/list-campaign-activities.md) **curl** ```bash curl "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities?types=MAIL_REPLIED,LINKEDIN_REPLIED&page=0" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const campaignId = '6650f1a2b839d80011c5a0f1'; for (let page = 0; ; page++) { const url = `https://api.emelia.io/advanced/campaigns/${campaignId}/activities?types=MAIL_REPLIED,LINKEDIN_REPLIED&page=${page}`; const res = await fetch(url, { headers: { Authorization: process.env.EMELIA_API_KEY } }); const { activities } = await res.json(); if (!activities?.length) break; for (const activity of activities) console.log(activity.event, activity.date); } ``` **Python** ```python campaign_id = "6650f1a2b839d80011c5a0f1" page = 0 while True: res = session.get( f"https://api.emelia.io/advanced/campaigns/{campaign_id}/activities", params={"types": "MAIL_REPLIED,LINKEDIN_REPLIED", "page": page}, ).json() if not res.get("activities"): break for activity in res["activities"]: print(activity["event"], activity["date"]) page += 1 ``` Activities are cached for up to 10 minutes. For a large campaign, the CSV export is faster than paging: `GET /advanced/campaigns/{campaignId}/export?type=replies` returns every reply in one file. See [Replies](https://docs.emelia.io/docs/replies.md). --- # Errors > The shape of an Emelia API error, what each HTTP status means today, the stable error codes, and how to retry safely. Source: https://docs.emelia.io/docs/errors ## The shape of an error The main API answers errors with a JSON body that always has an `error` message. Catalogued errors also carry a stable `code`, and sometimes `params`: ```json { "error": "You cannot add more than 40 LinkedIn contacts per day when your sequence contains a connection request", "code": "MC_LINKEDIN_DAILY_LIMIT_CONNECTION", "params": { "limit": 40 } } ``` Branch on `code` when there is one, never on the text of `error`: messages can be reworded, codes are never renamed. Most business errors have no code yet: rely on the HTTP status and show the message. The lists and contacts routes (`/lists/...`) are served by another service and use a `message` field instead, sometimes with a `code`: ```json { "message": "Request body too large: the limit is 5mb", "code": "PAYLOAD_TOO_LARGE", "params": { "limit": "5mb" } } ``` Their `401` is the exception: `{ "error": "Authentication required", "code": "AUTH_REQUIRED" }`. ## Status codes | Status | Meaning today | What to do | | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `200`, `201` | Success. Adding a contact that already exists also answers `200`: read `duplicate` in the body. | | | `400` | The request is invalid, a business rule refused it, **or the resource was not found**: the main API answers "not found" with a `400`, for example `{"error": "Campaign not found"}`. Validation errors carry `code: "INVALID_REQUEST"` and name the field, for example `"\"country\" is required"`. A malformed JSON body gets an HTML `400` page. | Fix the request. Do not retry it unchanged. | | `401` | Missing, unknown or disabled API key: `{"error": "Authentication required"}`. | See [Authentication](https://docs.emelia.io/docs/authentication.md). | | `404` | Lists and contacts: the list or contact does not exist, or belongs to another account (`{"message": "List not found"}`). Any route: an unknown path, answered with an HTML page. | Check the id, the path and the method. | | `409` | `ACCOUNT_DISCONNECTED`: the LinkedIn account used by the request is disconnected. | Reconnect the account in the app. | | `413` | Lists and contacts: the body is larger than 5 MB (`PAYLOAD_TOO_LARGE`). | Send fewer contacts per call. | | `422` | Lists and contacts: the body is well formed but not acceptable (a field holds an array, a batch has more than 100 contacts, an email and a LinkedIn URL in the same lookup). | Read `message`. | | `429` | `RATE_LIMITED`: LinkedIn itself slows the account down. | Retry later. | | `500` | **Your per minute quota is exceeded** (`{"error": "Internal server error"}`, no `Retry-After`), or an unexpected server error. | Wait for the next minute, then retry slower. See [Rate limits](https://docs.emelia.io/docs/rate-limits.md). | | `503` | `STATS_COMPUTING`: the statistics of the campaign are being computed, with `Retry-After: 30`. | Retry after the delay. | ### Routes with their own format * The statistics routes (`/stats/...`) answer `401` when the campaign is not found, and `401 {"error": "You must be logged in"}` on any internal error. A `401` there does not always mean a bad key: check the campaign id first. * `GET /webhook` answers `401 {"error": "You must be logged in"}` when reading the webhooks fails. * `GET /credits/usage` answers `{"success": false, "error": "..."}`. * The deliverability diagnosis (`POST /email-providers/warmup/check-health`) answers `{"success": false, "message": "..."}`. * The message of a send activity answers `404 {"error": "NOT_FOUND"}` or `422 {"error": "NO_MESSAGE"}`. * The LinkedIn scraper routes answer English sentences without code, all with a `400`. ## Out of credits Enrichment calls fail with a `400` and this message when your balance is too low: ```json { "error": "You do not have enough credits to perform this action" } ``` Stop the batch rather than retrying row by row, and top up your credits. See [Credits](https://docs.emelia.io/docs/credits.md). ## Error codes ### Starting a campaign `POST /advanced/campaigns/{campaignId}/start` runs these checks in this order, and stops at the first that fails: | Code | Meaning | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | `SUBSCRIPTION_REQUIRED` | A campaign needs an active subscription to start. | | `CAMPAIGN_NOT_FOUND` | No campaign with this id on your account. | | `NO_IDENTITY_SET` | The campaign has no identity (sender). | | `IDENTITY_FOREIGN_ACCOUNT` | An identity uses an account that does not belong to you. | | `LINKEDIN_AUDIO_MISSING` | A LinkedIn voice note step has no recording. | | `EMAIL_STEP_NO_ACTIVE_VERSION` | An email step has no active version. | | `EMAIL_VERSION_NO_CONTENT` | A version of an email step is empty. | | `EMAIL_VERSION_NO_SUBJECT` | The first email of the sequence needs a subject. | | `LINKEDIN_COOKIE_EXPIRED`, `LINKEDIN_SLOT_EXPIRED` | Reconnect the LinkedIn account, or renew its slot. | | `LINKEDIN_ACCOUNT_AMBIGUOUS` | Use a single LinkedIn account for all identities, or one per identity. | | `EMAIL_PROVIDER_DISCONNECTED`, `EMAIL_PROVIDER_EXPIRED` | A mailbox is disconnected, or its subscription expired. | | `IDENTITY_EMAIL_PROVIDER_MISSING` | The sequence has an email step, so every identity needs an email provider. | | `WHATSAPP_ACCOUNT_EXPIRED`, `WHATSAPP_SLOT_EXPIRED`, `WHATSAPP_ACCOUNT_AMBIGUOUS` | The same checks for WhatsApp accounts. | | `SCHEDULE_MISSING` | The campaign has no schedule. | | `MC_LINKEDIN_DAILY_LIMIT_CONNECTION` | More than 40 new LinkedIn contacts per day with an invitation in the sequence. | | `MC_LINKEDIN_DAILY_LIMIT` | More than 80 new LinkedIn contacts per day. | | `NO_RECIPIENTS` | No contact in the attached lists. | | `TOO_MANY_RECIPIENTS` | More than 30,000 recipients. | | `CAMPAIGN_ALREADY_RUNNING` | The campaign is already running. | | `RECIPIENTS_NO_EMAIL` | The sequence has an email step but no contact has an email. | | `RECIPIENTS_NO_LINKEDIN` | The sequence has a LinkedIn step but no contact has a LinkedIn profile URL. | ### Other codes | Code | Meaning | | ---------------------------------------------------- | --------------------------------------------------------------------- | | `INVALID_REQUEST` | The body or the query failed validation. The message names the field. | | `INVALID_PARAMETERS` | Invalid parameters. | | `PLANNED_START_INVALID`, `PLANNED_START_IN_PAST` | `plannedStart` is not a valid date, or is in the past. | | `EMAIL_PROVIDER_MISSING`, `LINKEDIN_ACCOUNT_MISSING` | No mailbox, or no LinkedIn account, to act with. | | `TRACKING_DOMAINS_REQUIRED` | Every email provider needs a tracking domain for this campaign. | | `WARMUP_PROVIDER_UNSUPPORTED` | Warmup cannot be enabled on this provider. | | `ACCOUNT_DISCONNECTED` | The LinkedIn account is disconnected (`409`). | | `RATE_LIMITED` | LinkedIn slows the account down (`429`). | | `STATS_COMPUTING` | Statistics are being computed (`503`). | ## Retrying safely * Retry `500` and `503` after a pause (60 seconds for the quota, the `Retry-After` value otherwise), a few times at most. * Do not retry a `4xx` unchanged: the answer will be the same. * Reads (`GET`) are safe to retry. * Adding a contact to a list is safe to retry when the contact has an email or a LinkedIn URL: the duplicate is detected. Without either, every retry creates a new contact. * Never retry a request that started a paid job (Email Finder, Phone Finder, Email Verifier) without checking the job first: you would pay twice. --- # Credits > What costs credits in the Emelia API, how much, when credits are refunded, and how to follow your consumption by API. Source: https://docs.emelia.io/docs/credits Sending campaigns and calling the API do not cost credits. Enrichment does: finding an email, finding a phone number, verifying an address. ## What each call costs | Action | Route | Cost | Refunded when | | --------------------------------------------------------- | -------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | [Email Finder](https://docs.emelia.io/docs/enrichment/email-finder.md) | `POST /tools/find/email` | 1 credit | No email is found, or the search could not start. Not refunded when the search times out. | | [Phone Finder](https://docs.emelia.io/docs/enrichment/phone-finder.md) | `POST /tools/find/phone` | 50 credits | No number is found, or the search could not start. A number already found in the last 6 months is served at once and costs 50 credits too. | | [Email Verifier](https://docs.emelia.io/docs/enrichment/email-verifier.md) | `POST /tools/verify/email` | 0.25 credit | Never, whatever the result. | | [Emails of a LinkedIn extraction](https://docs.emelia.io/docs/linkedin-scraper.md) | `POST /linkedin-scrappers/{id}/enrich` | 1 credit per email found | The credits of the contacts without email come back when the enrichment ends. | Credits are taken when the job starts, then given back as described above. Lists, contacts, campaigns and exports never cost credits. ## Your balance Your balance is shown in the Emelia app. When it is too low, enrichment calls fail with a `400`: ```json { "error": "You do not have enough credits to perform this action" } ``` Stop the batch at the first one: the next rows would fail the same way. [Your credits](https://v3.emelia.io/settings/credits) [See pricing](https://emelia.io/pricing) ## Follow your consumption by API The credits really consumed are listed, newest first, by: [`GET /credits/usage`](https://docs.emelia.io/docs/credits/list-credit-debits.md) **curl** ```bash curl "https://api.emelia.io/credits/usage?page=1&limit=50" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/credits/usage?page=1&limit=50', { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { items, total } = await res.json(); for (const debit of items) console.log(debit.createdAt, debit.action, debit.amount); ``` **Python** ```python import os import requests res = requests.get( "https://api.emelia.io/credits/usage", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, params={"page": 1, "limit": 50}, ) for debit in res.json()["items"]: print(debit["createdAt"], debit["action"], debit["amount"]) ``` ```json title="Response" { "success": true, "items": [ { "_id": "6650f1a2b839d80011c5f101", "amount": 1, "action": "email_finder", "createdAt": "2026-10-11T08:00:30.000Z" } ], "total": 1, "page": 1, "totalPages": 1 } ``` `page` starts at 1, `limit` is 50 by default and 200 at most. Refunded debits are not listed: the history shows what you really paid. ## Spend less * **Filter before you enrich.** Every row you remove before the finder is a credit you keep. * **Do not verify what the Email Finder returned.** An email found with `qualification: "valid"` was already checked. Verifying it again costs 0.25 credit for the same answer. * **Skip rows that cannot be found.** No company name means the Email Finder has nothing to search. No LinkedIn profile URL means the Phone Finder cannot run. * **Never start the same search twice.** A job that is still `running` is not lost: poll its `jobId` instead of sending the request again. See [Enrichment](https://docs.emelia.io/docs/enrichment.md). --- # List credit debits Source: https://docs.emelia.io/docs/credits/list-credit-debits Guide: [Credits](https://docs.emelia.io/docs/credits.md) `GET https://api.emelia.io/credits/usage` History of the credits really consumed (refunds and refunded debits are not listed), newest first. `page` is one based. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page` | integer | no | Default: `1`. Range: 1 to .... | | `limit` | integer | no | Default: `50`. Range: 1 to 200. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/credits/usage" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/credits/usage', { 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/credits/usage", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `items` | array of object | no | | | `items[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `items[].amount` | number | no | | | `items[].action` | string | no | For example `email_finder`, `phone_finder`, `email_verification`, `phone_finder_bulk`, `scrap_phone_enrichment`. | | `items[].meta` | object | no | | | `items[].createdAt` | string (date-time) | no | | | `total` | integer | no | | | `page` | integer | no | | | `totalPages` | integer | no | | ```json { "success": true, "items": [ { "_id": "6650f1a2b839d80011c5f101", "amount": 1, "action": "email_finder", "createdAt": "2026-10-11T08:00:30.000Z" } ], "total": 1, "page": 1, "totalPages": 1 } ``` ### 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" } ``` --- # Campaigns > Create, configure and launch multichannel campaigns by API. Cold email sequences, LinkedIn automation, WhatsApp and manual tasks, conditions, senders, schedule and tracking. Source: https://docs.emelia.io/docs/campaigns Every Emelia campaign is multichannel: email steps, LinkedIn steps, WhatsApp messages, manual tasks and conditions live in the same sequence. Its steps decide its channels, so a cold email campaign and a LinkedIn automation campaign are created the same way. A campaign is built in a few calls: 1. [Create the campaign](#1-create-the-campaign) 2. [Write the sequence](#2-write-the-sequence) 3. [Set the schedule](#3-set-the-schedule) 4. [Choose the senders](#4-choose-the-senders) 5. [Attach the contacts](#5-attach-the-contacts) 6. [Send yourself a test](#6-send-yourself-a-test) 7. [Start it](#7-start) The examples use this setup: **curl** ```bash export EMELIA_API_KEY="your_api_key" export CAMPAIGN_ID="6650f1a2b839d80011c5a0f1" ``` **JavaScript** ```js const emelia = async (method, path, body) => { const res = await fetch(`https://api.emelia.io${path}`, { method, headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: body ? JSON.stringify(body) : undefined, }); const json = await res.json(); if (!res.ok) throw new Error(`${res.status} ${json.code ?? ''} ${json.error ?? json.message}`); return json; }; ``` **Python** ```python import os import requests session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] def emelia(method, path, body=None): res = session.request(method, f"https://api.emelia.io{path}", json=body) data = res.json() if not res.ok: raise RuntimeError(f"{res.status_code} {data.get('code', '')} {data.get('error') or data.get('message')}") return data ``` ## 1. Create the campaign [`POST /advanced/campaigns`](https://docs.emelia.io/docs/campaigns/create-campaign.md) **curl** ```bash curl -X POST https://api.emelia.io/advanced/campaigns \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Q4 SaaS founders", "timeZone": "Europe/Paris"}' # {"success":true,"campaignId":"6650f1a2b839d80011c5a0f1"} ``` **JavaScript** ```js const { campaignId } = await emelia('POST', '/advanced/campaigns', { name: 'Q4 SaaS founders', timeZone: 'Europe/Paris', }); ``` **Python** ```python campaign_id = emelia("POST", "/advanced/campaigns", { "name": "Q4 SaaS founders", "timeZone": "Europe/Paris", })["campaignId"] ``` The name must be unique on your account. The campaign is created as a `DRAFT`, with a sequence that only holds the `START` step, and these defaults: | Setting | Default | | ------------------------------------------ | -------------------------------------------------------------------------- | | `timeZone` | `Europe/Brussels`, unless you send another IANA zone | | `days` | Monday to Friday, `[0, 1, 2, 3, 4]` (0 is Monday, 6 is Sunday) | | `start`, `end` | `08:00` to `17:00` | | `dailyEmailAdded` | 35 new contacts per day in the email part | | `dailyEmailLimit` | 100 emails per day | | `dailyLinkedinAdded`, `dailyWhatsappAdded` | 20 new contacts per day | | `trackOpens`, `trackLinks` | off | | `eventToStop` | a reply by email, LinkedIn or WhatsApp stops the sequence for that contact | ## 2. Write the sequence The sequence is a **tree** of steps, sent whole in `steps`: an object, the root of the tree, not an array. The call replaces the whole tree, and the campaign must not be running. [`PATCH /advanced/campaigns/{campaignId}/steps`](https://docs.emelia.io/docs/campaigns/replace-sequence.md) ### A cold email sequence Three emails, three then five days apart: **curl** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/$CAMPAIGN_ID/steps" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d @sequence.json ``` **JavaScript** ```js import { readFile } from 'node:fs/promises'; const sequence = JSON.parse(await readFile('sequence.json', 'utf8')); await emelia('PATCH', `/advanced/campaigns/${campaignId}/steps`, sequence); ``` **Python** ```python import json with open("sequence.json") as f: sequence = json.load(f) emelia("PATCH", f"/advanced/campaigns/{campaign_id}/steps", sequence) ``` ```json title="sequence.json" { "steps": { "_id": "START", "stepType": "START", "delay": { "amount": 0, "unit": "DAYS" }, "next": { "stepType": "EMAIL", "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "subject": "{{companyName}} and Postgres 16", "message": "

Hi {{firstName}},

I saw {{icebreaker}}. ...

{{signature}}

", "disabled": false } ], "next": { "stepType": "EMAIL", "delay": { "amount": 3, "unit": "DAYS" }, "versions": [ { "subject": "", "message": "

Following up, {{firstName}}. ...

{{signature}}

Unsubscribe

", "disabled": false } ], "next": { "stepType": "EMAIL", "delay": { "amount": 5, "unit": "DAYS" }, "versions": [ { "subject": "", "message": "

Last note from me, {{firstName}}. ...

{{signature}}

Unsubscribe

", "disabled": false } ] } } } } } ``` * **The body is HTML.** One `

` per paragraph, `

` for an empty line. * **An empty subject keeps the thread**: the follow up arrives as a reply to the previous email. The first email needs a subject. * **Several `versions` on an `EMAIL` step make an A/B test**: each contact gets one of them. `disabled: true` takes a version out of the rotation. * **`cc`** on an `EMAIL` step copies an address, which can be a variable such as `{{assistantEmail}}`. ### A LinkedIn automation sequence Visit the profile, send an invitation, then two messages once it is accepted. Send it with the same call. ```json { "steps": { "_id": "START", "stepType": "START", "delay": { "amount": 0, "unit": "DAYS" }, "next": { "stepType": "VISIT", "delay": { "amount": 0, "unit": "DAYS" }, "versions": [{}], "next": { "stepType": "CONNECTION", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [{ "message": "" }], "next": { "stepType": "MESSAGE", "delay": { "amount": 2, "unit": "HOURS" }, "versions": [ { "message": "Thanks for connecting, {{firstName}}. I work with teams like {{companyName}} on ..." }, { "message": "{{firstName}}, would a 15 minute call next week make sense?" } ] } } } } } ``` * An empty note on `CONNECTION` sends the invitation without a message. * A `MESSAGE` step placed right after a `CONNECTION` waits for the invitation to be accepted, for up to a year. * On `MESSAGE` and `WHATSAPP` steps, the versions are successive messages sent in order, not an A/B test. * `dailyLinkedinAdded` is capped at 40 new contacts per day when the sequence sends invitations, 80 otherwise. ### A multichannel sequence with a condition LinkedIn first. If the invitation is accepted within 5 days, a LinkedIn message. If not, and the contact has an email, two emails. ```json { "steps": { "_id": "START", "stepType": "START", "delay": { "amount": 0, "unit": "DAYS" }, "next": { "stepType": "CONNECTION", "delay": { "amount": 0, "unit": "DAYS" }, "versions": [{ "message": "" }], "next": { "stepType": "CONDITION", "delay": { "amount": 5, "unit": "DAYS" }, "conditions": { "field": "ACCEPTED", "operator": "EQUAL", "value": "true" }, "yes": { "stepType": "MESSAGE", "delay": { "amount": 2, "unit": "HOURS" }, "versions": [{ "message": "Thanks for connecting, {{firstName}}. ..." }] }, "no": { "stepType": "CONDITION", "delay": { "amount": 1, "unit": "HOURS" }, "conditions": { "field": "$email", "operator": "IS_NOT_EMPTY", "value": "" }, "yes": { "stepType": "EMAIL", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [{ "subject": "{{companyName}} and Postgres 16", "message": "

Hi {{firstName}},

...

{{signature}}

", "disabled": false }], "next": { "stepType": "EMAIL", "delay": { "amount": 4, "unit": "DAYS" }, "versions": [{ "subject": "", "message": "

...

{{signature}}

Unsubscribe

", "disabled": false }] } }, "no": { "stepType": "END_OF_CAMPAIGN", "delay": { "amount": 0, "unit": "DAYS" } } } } } } } ``` ### Step and version ids: ObjectIds, never UUIDs > **Leave the ids out.** The `_id` of a step or of a version is a MongoDB **ObjectId**: 24 lowercase hexadecimal characters, like `66f2b4c19e7a3d5b8c0e1f24`. Never a UUID, never a label like `"email-1"`. > > **The simplest is to leave them out.** The server generates one for every step and every version that has none. The only `_id` you write is `"START"` on the root. The API accepts any string without an error, so a wrong id is not caught when you save the sequence. If you must write ids yourself, for example to keep a reference in your system, generate real ObjectIds: **curl** ```bash # 24 lowercase hexadecimal characters openssl rand -hex 12 ``` **JavaScript** ```js import { randomBytes } from 'node:crypto'; const objectId = () => randomBytes(12).toString('hex'); // 24 lowercase hex characters ``` **Python** ```python import secrets object_id = secrets.token_hex(12) # 24 lowercase hex characters ``` ### Anatomy of a step | Field | Description | | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `_id` | Leave it out on new steps. Keep it on steps that already ran, see [Editing a running campaign](#edit-a-campaign-that-already-sent). | | `stepType` | What the step does, see below. | | `delay` | Wait before this step, counted from the previous action: `{ "amount": 3, "unit": "DAYS" }`. `unit` is `MINUTES`, `HOURS` or `DAYS`. The delay of the first action is ignored: new contacts enter according to the daily quotas. With `DAYS`, the days that are not sending days are skipped. | | `versions` | The content of the step. Give every action step at least one version, even `VISIT` and `LIKE`, which take an empty one: `"versions": [{}]`. | | `next` | The following step. Absent on the last one. | | `conditions`, `yes`, `no` | Only on `CONDITION` steps. A missing branch ends the sequence for the contacts that fall into it. | | `identities` | Legacy field, not read when sending: send `[]` or leave it out. Senders are set on the campaign. | | `stepType` | Channel | What it does | | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `START` | | The root. Always `"_id": "START"`. | | `EMAIL` | Email | Sends an email: `versions[].subject` and `versions[].message` (HTML). | | `CONNECTION` | LinkedIn | Sends an invitation, with an optional note in `versions[].message`. | | `MESSAGE` | LinkedIn | Sends a message to a contact you are connected with, or a voice note, see [Attachments and voice notes](#attachments-and-voice-notes). | | `INMAIL` | LinkedIn | Sends an InMail, with a `subject` and a `message`. | | `VISIT` | LinkedIn | Visits the profile. `"versions": [{}]`. | | `LIKE` | LinkedIn | Likes a recent post of the contact. `"versions": [{}]`. | | `WHATSAPP` | WhatsApp | Sends a WhatsApp message (plain text). | | `TASK` | Manual | Creates a task for a human, see [Manual tasks](#manual-tasks). The label is `versions[].subject`. | | `CONDITION` | | Branches on an event or a field, see below. | | `END_OF_CAMPAIGN` | | Ends the sequence for the contact. | There is no `WAIT` step: a wait is the `delay` of the next step. LinkedIn step types have no `LINKEDIN_` prefix. `FOLLOW`, `AUDIO` and `API_CALL` exist in the model and are accepted when you save the sequence, but nothing runs them: no action happens and the step is never marked as done. Do not use them. A voice note is a `MESSAGE` step, see [Attachments and voice notes](#attachments-and-voice-notes). ### Conditions A `CONDITION` step has a `conditions` object, a `yes` branch and a `no` branch. `field` takes two forms: | Form | Examples | `operator` and `value` | | --------------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | An event, in capitals | `ACCEPTED`, `MAIL_REPLIED`, `LINKEDIN_REPLIED`, `OPENED`, `CLICKED`, `UNSUBSCRIBED`, `BOUNCED`, `TASK_COMPLETED` | `EQUAL` and `"true"` | | A contact field, with a `$` | `$email`, `$jobTitle`, `$icebreaker` | `EQUAL`, `DOES_NOT_EQUAL`, `CONTAINS`, `DOES_NOT_CONTAIN`, `GREATER`, `LOWER`, `IS_EMPTY`, `IS_NOT_EMPTY` | * **The `delay` of an event condition is its observation window.** "Accepted within 5 days?" keeps the contacts that have not accepted on hold for 5 days, then sends them down `no`. A field condition is evaluated at once. * **Only the operators above are evaluated.** Any other value, for example `NOT_EQUAL` or `GREATER_THAN`, is saved without error and always evaluates to false: everybody goes down `no`. * **`{{email}}` is not a valid field**: write `$email`. * **`OPENED` and `CLICKED` need tracking** (`trackOpens`, `trackLinks`). Without it the event is never recorded and everybody goes down `no`. * **Branches never join again.** A step needed after both branches is written in each of them. ### Variables Write variables as `{{name}}` in subjects and messages: * the contact fields `firstName`, `lastName`, `fullName`, `email`, `secondaryEmail`, `phone`, `mobilePhone`, `jobTitle`, `seniority`, `department`, `gender`, `age`, `language`, `linkedinUrlProfile`, `twitterUrl`, `country`, `region`, `city`, `postalCode`, `address`, `timezone`, `yearsOfExperience`, `education`, `bio`, `companyName`; * every custom variable of your contacts, for example `{{icebreaker}}`; * the company fields, with `companyCountry` and `companyCity` for the country and city of the company; * `{{signature}}`, the signature of the sending mailbox, and `{{unsubscribe_link}}`, the unsubscribe URL to put in a link. Messages also support spintax: `{Hi|Hello|Hey} {{firstName}}` picks one of the options for each contact. Check that every contact has a value for the variables you use, then send yourself a test (step 6) before you start. ### Attachments and voice notes Upload the files first, then reference their URL in `versions[].attachments` of an `EMAIL`, `MESSAGE` or `WHATSAPP` step. Uploaded files get a public URL: anyone with the link can download them. [`POST /advanced/attachments`](https://docs.emelia.io/docs/campaigns/upload-message-attachments.md) **curl** ```bash curl -X POST https://api.emelia.io/advanced/attachments \ -H "Authorization: $EMELIA_API_KEY" \ -F "attachments=@./brochure.pdf" ``` **JavaScript** ```js import { readFile } from 'node:fs/promises'; const form = new FormData(); form.append('attachments', new Blob([await readFile('brochure.pdf')]), 'brochure.pdf'); const res = await fetch('https://api.emelia.io/advanced/attachments', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, body: form, }); const { attachments } = await res.json(); // [{ url, name }] ``` **Python** ```python with open("brochure.pdf", "rb") as f: res = session.post( "https://api.emelia.io/advanced/attachments", files={"attachments": ("brochure.pdf", f)}, ) attachments = res.json()["attachments"] # [{"url": ..., "name": ...}] ``` Up to 3 files of 20 MB per call. A LinkedIn voice note goes through its own route (webm, 10 MB and 5 minutes at most), which returns its `url` and its `duration` in milliseconds, to attach with `"mediaType": "AUDIO"`: [`POST /advanced/linkedin/audio`](https://docs.emelia.io/docs/campaigns/upload-linkedin-voice-note.md) The voice note itself is a `MESSAGE` step, not a step type of its own: put the recording as the attachment of its first version, with the `name`, `url` and `duration` the upload returned. `duration` stays in milliseconds (`32000` is 32 seconds). ```json title="Voice note step" { "stepType": "MESSAGE", "delay": { "amount": 2, "unit": "DAYS" }, "versions": [ { "message": "", "attachments": [ { "name": "voice-note.m4a", "url": "https://...", "mediaType": "AUDIO", "duration": 32000 } ] } ] } ``` A `MESSAGE` step whose first attachment has no `url` blocks the start with `LINKEDIN_AUDIO_MISSING`. ## 3. Set the schedule [`PATCH /advanced/campaigns/{campaignId}/settings`](https://docs.emelia.io/docs/campaigns/update-campaign-settings.md) **curl** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/$CAMPAIGN_ID/settings" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "settings": { "timeZone": "Europe/Paris", "days": [0, 1, 2, 3, 4], "start": "09:00", "end": "17:30", "dailyEmailAdded": 20, "dailyEmailLimit": 60, "trackOpens": false, "trackLinks": false, "blacklistUnsub": true, "ignoreAutoReplies": true } }' ``` **JavaScript** ```js await emelia('PATCH', `/advanced/campaigns/${campaignId}/settings`, { settings: { timeZone: 'Europe/Paris', days: [0, 1, 2, 3, 4], start: '09:00', end: '17:30', dailyEmailAdded: 20, dailyEmailLimit: 60, trackOpens: false, trackLinks: false, blacklistUnsub: true, ignoreAutoReplies: true, }, }); ``` **Python** ```python emelia("PATCH", f"/advanced/campaigns/{campaign_id}/settings", { "settings": { "timeZone": "Europe/Paris", "days": [0, 1, 2, 3, 4], "start": "09:00", "end": "17:30", "dailyEmailAdded": 20, "dailyEmailLimit": 60, "trackOpens": False, "trackLinks": False, "blacklistUnsub": True, "ignoreAutoReplies": True, }, }) ``` The fields you send in `settings` are merged into the current settings. Always send `start`, `end`, `trackOpens`, `trackLinks` and `blacklistUnsub`: the call fails without them. The simplest is to read the campaign, change its `schedule` and send it back. | Setting | What it does | | ---------------------------------- | -------------------------------------------------------------------------------------------------- | | `dailyEmailAdded` | New contacts entering the email part per day, 1 to 500. | | `dailyEmailLimit` | Emails sent per day, follow ups included, 1 to 500, and at least `dailyEmailAdded`. | | `dailyLinkedinAdded` | New contacts per day on LinkedIn, 1 to 80, 40 at most when the sequence sends invitations. | | `dailyWhatsappAdded` | New contacts per day on WhatsApp, 1 to 30. | | `days`, `start`, `end`, `timeZone` | When the campaign acts: days from 0 (Monday) to 6 (Sunday), hours as `HH:mm`, an IANA time zone. | | `blacklistUnsub` | A contact who unsubscribes is added to your [blacklist](https://docs.emelia.io/docs/blacklist.md). | | `ignoreAutoReplies` | Out of office answers do not count as replies. | | `eventToStop` | The events that end the sequence for a contact. Bounced and unsubscribed contacts always stop. | | `bcc` | An address copied on every email, for example your CRM dropbox. Empty string to disable. | | `alwaysOn` | The campaign keeps running when every contact is done, so that contacts added later are processed. | ## 4. Choose the senders [`PATCH /advanced/campaigns/{campaignId}/identities`](https://docs.emelia.io/docs/campaigns/set-sender-identities.md) An **identity** is a sender: a name, plus an email provider, a LinkedIn account, a WhatsApp account, or several of them. For each step, Emelia picks the least used identity that has an account on the channel. Get the ids of your mailboxes with [List email providers](https://docs.emelia.io/docs/email-providers/list-email-providers.md) (`_id`, and skip the ones marked `disconnected`). LinkedIn and WhatsApp accounts are connected in the Emelia app; there is no documented route to list them yet: set them in the app, or reuse the ids found in the `identities` of an existing campaign (`GET /advanced/campaigns`). **curl** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/$CAMPAIGN_ID/identities" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"identities": [{"name": "Marie Martin", "email": "6650f1a2b839d80011c5a0aa", "linkedin": "6650f1a2b839d80011c5a0bb"}]}' ``` **JavaScript** ```js await emelia('PATCH', `/advanced/campaigns/${campaignId}/identities`, { identities: [{ name: 'Marie Martin', email: '6650f1a2b839d80011c5a0aa', linkedin: '6650f1a2b839d80011c5a0bb' }], }); ``` **Python** ```python emelia("PATCH", f"/advanced/campaigns/{campaign_id}/identities", { "identities": [{"name": "Marie Martin", "email": "6650f1a2b839d80011c5a0aa", "linkedin": "6650f1a2b839d80011c5a0bb"}], }) ``` The array replaces the identities of the campaign. With an email step, every identity needs an email provider. With a LinkedIn step, use one LinkedIn account shared by all identities, or one per identity: otherwise the start fails with `LINKEDIN_ACCOUNT_AMBIGUOUS`. On an agency account, attach only the mailboxes of this client. ## 5. Attach the contacts Contacts live in [lists](https://docs.emelia.io/docs/lists.md). Attach one or several lists, and optionally lists to exclude: [`PATCH /advanced/campaigns/{campaignId}/recipients`](https://docs.emelia.io/docs/campaigns/set-contact-lists.md) **curl** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/$CAMPAIGN_ID/recipients" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"lists": ["66f1c0ffee5a7d3b2c4e8a10"]}' ``` **JavaScript** ```js await emelia('PATCH', `/advanced/campaigns/${campaignId}/recipients`, { lists: ['66f1c0ffee5a7d3b2c4e8a10'], }); ``` **Python** ```python emelia("PATCH", f"/advanced/campaigns/{campaign_id}/recipients", { "lists": ["66f1c0ffee5a7d3b2c4e8a10"], }) ``` `lists` replaces the whole selection. A contact whose email or LinkedIn URL is in one of the `excludedLists` is left out. This call works on a running campaign, and every contact added to an attached list later joins the campaign on its own. A campaign starts with 30,000 recipients at most. To add one contact straight to a campaign, without managing a list, use [Add a contact to a campaign](https://docs.emelia.io/docs/campaigns/add-contact-to-campaign.md). Emelia stores it in a list named "\ contacts from API". A running campaign schedules it at once, and a finished campaign starts again on its own. Only `email`, `linkedinUrlProfile`, `phone`, `firstName` and `lastName` are native fields there; every other key becomes a custom variable, and every value must be a string. [`POST /advanced/campaign/contacts`](https://docs.emelia.io/docs/campaigns/add-contact-to-campaign.md) ## 6. Send yourself a test [`POST /advanced/campaigns/{campaignId}/test-email`](https://docs.emelia.io/docs/campaigns/send-test-email.md) Take the `_id` of the email step and of its version from [Get a campaign](https://docs.emelia.io/docs/campaigns/get-campaign.md), after step 2. The test uses the variables of a random contact of the campaign. **curl** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/$CAMPAIGN_ID/test-email" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"stepId": "6650f1a2b839d80011c5c001", "versionId": "6650f1a2b839d80011c5c002", "email": "you@yourcompany.com"}' ``` **JavaScript** ```js await emelia('POST', `/advanced/campaigns/${campaignId}/test-email`, { stepId: '6650f1a2b839d80011c5c001', versionId: '6650f1a2b839d80011c5c002', email: 'you@yourcompany.com', }); ``` **Python** ```python emelia("POST", f"/advanced/campaigns/{campaign_id}/test-email", { "stepId": "6650f1a2b839d80011c5c001", "versionId": "6650f1a2b839d80011c5c002", "email": "you@yourcompany.com", }) ``` ## 7. Start [`POST /advanced/campaigns/{campaignId}/start`](https://docs.emelia.io/docs/campaigns/start-campaign.md) **curl** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/$CAMPAIGN_ID/start" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js await emelia('POST', `/advanced/campaigns/${campaignId}/start`); ``` **Python** ```python emelia("POST", f"/advanced/campaigns/{campaign_id}/start") ``` The start needs an active subscription and runs every check at once. The first that fails is returned with a stable [error code](https://docs.emelia.io/docs/errors.md#starting-a-campaign): `NO_IDENTITY_SET`, `EMAIL_VERSION_NO_SUBJECT`, `NO_RECIPIENTS`, `RECIPIENTS_NO_EMAIL`... Contacts are scheduled in the background after the response. To start later, set a date with [Schedule a planned start](https://docs.emelia.io/docs/campaigns/schedule-planned-start.md) (`{"plannedStart": "2026-10-20T08:00:00.000Z"}`, at least in the future), and send `null` to cancel it. The same checks run when you schedule. ## Lifecycle | Status | How you get there | What you can change | | ---------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------- | | `DRAFT` | Create or duplicate | Everything | | `RUNNING` | [Start](https://docs.emelia.io/docs/campaigns/start-campaign.md), or a planned start | Only the attached lists | | `PAUSED` | [Pause](https://docs.emelia.io/docs/campaigns/pause-campaign.md), or [Reactivate](https://docs.emelia.io/docs/campaigns/reactivate-campaign.md) a finished campaign | Everything, then start again | | `FINISHED` | Every contact is done, or [Mark as finished](https://docs.emelia.io/docs/campaigns/mark-campaign-as-finished.md) | Reactivate it first | | `ARCHIVED` | [Archive](https://docs.emelia.io/docs/campaigns/archive-campaign.md), on a campaign that is not running | Hidden from the list of campaigns | Pausing deletes the scheduled actions, the pending conditions and the pending manual tasks: they are created again when the campaign restarts and contacts reach those steps. [Delete a campaign](https://docs.emelia.io/docs/campaigns/delete-campaign.md) is irreversible; the lists and contacts stay. ### Edit a campaign that already sent 1. Pause it: `POST /advanced/campaigns/{campaignId}/pause`. 2. Read it: `GET /advanced/campaigns/{campaignId}`. The tree is in `campaign.steps`. 3. Change what you need **in the tree you read**, and keep the `_id` of every existing step and version. History, statistics per version and the position of each contact in the sequence are attached to those ids: new ids make contacts receive steps again. 4. Add new steps without `_id`. 5. Send the whole tree back, then start the campaign again. ## Follow the campaign | Need | Route | | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | Status, sequence, settings, lists | [Get a campaign](https://docs.emelia.io/docs/campaigns/get-campaign.md) | | Sent, opened, clicked, replied, bounced, globally and per step | [Get campaign statistics](https://docs.emelia.io/docs/campaigns/get-campaign-statistics.md) | | How many contacts passed or wait at each step | [Get per step progress](https://docs.emelia.io/docs/campaigns/get-per-step-progress.md), then [List the contacts of a step](https://docs.emelia.io/docs/campaigns/list-contacts-of-step.md) | | The feed of events and the replies | [Replies](https://docs.emelia.io/docs/replies.md) | | Everything in a CSV (`contacts`, `activities` or `replies`) | [Export as CSV](https://docs.emelia.io/docs/campaigns/export-as-csv.md) | | Real time events | [Webhooks](https://docs.emelia.io/docs/webhooks.md) | The statistics are computed on demand and cached. An unknown campaign answers `401` there, not `400`: check the id before you check the key. ### Manual tasks A `TASK` step waits for a human, for example a phone call. List what is waiting, then close each task with `ok` (done, event `TASK_COMPLETED`) or `ko` (failed, event `TASK_FAILED`). A condition on `TASK_COMPLETED` can route each case. [`GET /advanced/campaigns/{campaignId}/manual-tasks`](https://docs.emelia.io/docs/campaigns/list-pending-manual-tasks.md) [`POST /advanced/campaigns/{campaignId}/manual-tasks/status`](https://docs.emelia.io/docs/campaigns/complete-manual-task.md) **curl** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/$CAMPAIGN_ID/manual-tasks/status" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"contactId": "6650f1a2b839d80011c5d001", "taskId": "6650f1a2b839d80011c5c010", "status": "ok"}' ``` **JavaScript** ```js await emelia('POST', `/advanced/campaigns/${campaignId}/manual-tasks/status`, { contactId: '6650f1a2b839d80011c5d001', taskId: '6650f1a2b839d80011c5c010', status: 'ok', }); ``` **Python** ```python emelia("POST", f"/advanced/campaigns/{campaign_id}/manual-tasks/status", { "contactId": "6650f1a2b839d80011c5d001", "taskId": "6650f1a2b839d80011c5c010", "status": "ok", }) ``` `taskId` is the `_id` of the `TASK` step. A task can be completed once. ## Legacy campaigns The `/emails/...` and `/linkedin/...` campaign routes belong to the previous campaign system, where a campaign was email only or LinkedIn only. They still work and are documented under [Legacy](https://docs.emelia.io/docs/campaigns/legacy.md), but every new integration should use the routes on this page. --- # List campaigns Source: https://docs.emelia.io/docs/campaigns/list-campaigns Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `GET https://api.emelia.io/advanced/campaigns` Returns all multichannel campaigns of the account except archived ones, newest first. No pagination. Use `all=true` to get an extra first item `ALL_CAMPAIGNS` (used to register a webhook on every campaign). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `all` | enum: `true`, `false` | no | Prepend the synthetic `ALL_CAMPAIGNS` entry. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns', { 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/campaigns", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaigns` | array of object | no | | | `campaigns[]._id` | string | no | Campaign id. With `all=true` the first item is a synthetic entry whose `_id` is `"ALL_CAMPAIGNS"`. | | `campaigns[].name` | string | no | | | `campaigns[].status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaigns[].createdAt` | string (date-time) | no | | | `campaigns[].startAt` | string (date-time) | no | | | `campaigns[].plannedStart` | string (date-time) | no | | | `campaigns[].identities` | array of object | no | | | `campaigns[].hasDisconnectedLinkedin` | boolean | no | | | `campaigns[].hasDisconnectedEmail` | boolean | no | | ```json { "success": true, "campaigns": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Acme Q4 outreach", "status": "RUNNING", "createdAt": "2026-10-01T08:00:00.000Z", "startAt": "2026-10-02T07:00:00.000Z", "identities": [ { "_id": "6650f1a2b839d80011c5a0f9", "email": "6650f1a2b839d80011c5a0aa", "linkedin": "6650f1a2b839d80011c5a0bb" } ], "hasDisconnectedLinkedin": false, "hasDisconnectedEmail": false } ] } ``` ### 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" } ``` --- # Create a campaign Source: https://docs.emelia.io/docs/campaigns/create-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns` Creates an empty multichannel campaign in `DRAFT` status with default settings (35 new email contacts per day, 100 emails per day, 20 LinkedIn contacts per day, Monday to Friday 08:00 to 17:00, tracking off) and a sequence that only contains the START step. The name must be unique in your account. Webhooks registered with `campaignId: "ALL_CAMPAIGNS"` and `type: "advanced"` are copied onto the new campaign. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | Unique campaign name. | | `timeZone` | string | no | IANA time zone. Ignored when unknown (the default `Europe/Brussels` is kept). | ```json { "name": "Acme Q4 outreach", "timeZone": "Europe/Paris" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Q4 outreach", "timeZone": "Europe/Paris" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Acme Q4 outreach", "timeZone": "Europe/Paris" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Acme Q4 outreach", "timeZone": "Europe/Paris" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaignId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "campaignId": "6650f1a2b839d80011c5a0f1" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Get a campaign Source: https://docs.emelia.io/docs/campaigns/get-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `GET https://api.emelia.io/advanced/campaigns/{campaignId}` Returns the full campaign: schedule (settings), sequence tree (`steps`), identities, attached lists with their contact count, status and dates. Read it before any `PATCH`: the steps and settings routes replace what you send. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1', { 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/campaigns/6650f1a2b839d80011c5a0f1", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaign` | object | no | Full multichannel campaign as stored (internal fields such as `__v`, `hubspotIntegration`, `pipedriveIntegration` and `zapierData` are returned too). | | `campaign._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.tagIds` | array of string | no | | | `campaign.identities` | array of object | no | | | `campaign.schedule` | object | no | Sending settings of a multichannel campaign. Defaults of a new campaign are shown. | | `campaign.steps` | object | no | One node of the sequence tree. The root is always a `START` step. Each node points to the next one with `next`; a `CONDITION` node branches with `yes` and `no` (a missing branch ends the sequence for the contacts that fall into it). `END_OF_CAMPAIGN` stops the sequence. EMAIL versions are A/B variants (one version per contact); MESSAGE and WHATSAPP versions are successive messages sent in order. A MESSAGE step placed after a CONNECTION step waits for the invitation to be accepted. | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | Webhooks registered on this campaign. | | `campaign.hubspotIntegration` | object | no | | | `campaign.pipedriveIntegration` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | | `campaign.__v` | integer | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "tagIds": [ "6650f1a2b839d80011c5a0f1" ], "identities": [ { "_id": "6650f1a2b839d80011c5a0f1", "userId": "6650f1a2b839d80011c5a0f1", "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0f1", "linkedin": "6650f1a2b839d80011c5a0f1", "whatsapp": "6650f1a2b839d80011c5a0f1" } ], "schedule": { "dailyEmailAdded": 35, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "dailyWhatsappAdded": 20, "bcc": "", "trackLinks": false, "trackOpens": false, "timeZone": "Europe/Brussels", "days": [ 0, 1, 2, 3, 4 ], "start": "08:00", "end": "17:00", "blacklistUnsub": false, "excludeAlreadyMessaged": true, "eventToStop": [ "VISITED" ], "alwaysOn": false, "ignoreAutoReplies": false }, "steps": { "_id": "6650f1a2b839d800552fc201", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800d4064809" ], "versions": [ { "_id": "6650f1a2b839d800ed4363dc", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [ { "mediaType": "MEDIA" } ] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800f83a5090", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800c85e3a84" ], "versions": [ { "_id": "6650f1a2b839d8001daab8ff", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80040f13c3f", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800c39d462f" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80035cb8e8a", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8008283738e", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800fabfab18", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800cf9b2e9d", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800cd9659c5" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8000feddd84", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8006252b58c", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800f02835ee", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d80090000761", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800df7d5a69" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800e7c06cf0", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d80044432310", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8001cc27212", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "yes": { "_id": "6650f1a2b839d8002fcb7330", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80073add4a4" ], "versions": [ { "_id": "6650f1a2b839d800501b769f", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8008e0454df", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8000fad334f" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8005ea5a9aa", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8002c0be9ae", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800a7cdff38", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d8009c26aa3d", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8005f17e165" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800b36aea24", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800e02962ac", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80027a94e0e", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d80093737481", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800f3adb689" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8009dccc810", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800a937cdb0", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800a9f44632", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "no": { "_id": "6650f1a2b839d800b74f7ab2", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800d01a715e" ], "versions": [ { "_id": "6650f1a2b839d80015726ab1", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800919d6c25", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8001b9fadcd" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800afc51a2c", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800a0db0d14", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800cc259cb6", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800271026d7", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8005e80ef67" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8004728f7c2", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800f6ba16e6", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8002a3b9db0", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d8001f510c4b", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8004a565f5b" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8004b91260e", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d80018e2a0ca", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800cd0fbfc4", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } } }, "recipients": { "lists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "excludedLists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "processing": true }, "zapierData": [ { "_id": "6650f1a2b839d80011c5a0f1", "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "hubspotIntegration": {}, "pipedriveIntegration": {}, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Unknown campaign or campaign of another account (no 404 is used). | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Replace the sequence Source: https://docs.emelia.io/docs/campaigns/replace-sequence Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `PATCH https://api.emelia.io/advanced/campaigns/{campaignId}/steps` Replaces the WHOLE sequence tree of the campaign. Always `GET` the campaign first, modify the `steps` object and send the complete tree back. The campaign must not be RUNNING (pause it first). Missing step and version `_id`s are generated by the server; keep existing ids on a campaign that already ran, otherwise history, statistics and the position of contacts in the sequence are lost and contacts may receive steps again. Content is not validated here: empty email versions, missing subjects or audio are only reported by `POST .../start`. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `steps` | object | yes | One node of the sequence tree. The root is always a `START` step. Each node points to the next one with `next`; a `CONDITION` node branches with `yes` and `no` (a missing branch ends the sequence for the contacts that fall into it). `END_OF_CAMPAIGN` stops the sequence. EMAIL versions are A/B variants (one version per contact); MESSAGE and WHATSAPP versions are successive messages sent in order. A MESSAGE step placed after a CONNECTION step waits for the invitation to be accepted. | | `steps._id` | string | no | Step identifier, generated by the server (ObjectId string) when missing; the root is `"START"`. Any unique string is accepted. Never regenerate the ids of a campaign that already ran. | | `steps.stepType` | enum: `START`, `EMAIL`, `CONNECTION`, `MESSAGE`, `INMAIL`, `VISIT`, `LIKE`, `FOLLOW`, `AUDIO`, `WHATSAPP`, `TASK`, `CONDITION`, `API_CALL`, `END_OF_CAMPAIGN` | yes | `FOLLOW`, `AUDIO` and `API_CALL` are accepted when saved, but nothing runs them: no action happens and the step is never marked as done. Do not use them. A LinkedIn voice note is a `MESSAGE` step whose first version carries an attachment with `mediaType: "AUDIO"`, uploaded with `POST /advanced/linkedin/audio`. | | `steps.delay` | object | yes | Wait time after the previous action before this step runs. The delay of the first action of the sequence is ignored (new contacts enter according to the daily quotas). With `DAYS`, days that are not in `schedule.days` are skipped. On a `CONDITION` step the delay is the observation window. | | `steps.delay.amount` | number | yes | Range: 0 to .... | | `steps.delay.unit` | enum: `MINUTES`, `HOURS`, `DAYS` | yes | | | `steps.identities` | array of string | no | Legacy field, not read when sending. Send `[]`. Sending accounts are chosen automatically among the campaign identities. | | `steps.versions` | array of object | no | Content of EMAIL, CONNECTION, MESSAGE, INMAIL, WHATSAPP and TASK steps. | | `steps.versions[]._id` | string | no | Version identifier. Generated by the server (ObjectId string) when missing. Keep existing ids when you update a campaign that already ran: statistics and history are attached to them. | | `steps.versions[].subject` | string | no | EMAIL: subject. Leave it empty on a follow up email to reply in the same thread ("Re: previous subject"). INMAIL: subject. TASK: the task label shown in manual tasks. | | `steps.versions[].message` | string | no | EMAIL: HTML body. MESSAGE, INMAIL, CONNECTION (invitation note) and WHATSAPP: plain text. Supports `{{variable}}` placeholders and `{a\|b\|c}` spintax. | | `steps.versions[].disabled` | boolean | no | Excludes the version from A/B rotation (EMAIL). | | `steps.versions[].rawHtml` | boolean | no | Stored but not read by the multichannel sending path. | | `steps.versions[].attachments` | array of object | no | | | `steps.cc` | string | no | EMAIL only: carbon copy address, may be a variable such as `{{assistantEmail}}`; ignored when the resolved value is not a valid email. | | `steps.conditions` | object | no | | | `steps.conditions.field` | string | yes | Either an activity condition in UPPER CASE (`ACCEPTED`, `LINKEDIN_REPLIED`, `MAIL_REPLIED`, `OPENED`, `CLICKED`, `UNSUBSCRIBED`, `BOUNCED`, `TASK_COMPLETED`), evaluated within the step delay window, or a contact field prefixed with `$` (`$firstName`, `$jobTitle`, `$myCustomVariable`), evaluated immediately. | | `steps.conditions.operator` | enum: `EQUAL`, `DOES_NOT_EQUAL`, `GREATER`, `LOWER`, `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` | yes | Only these values are evaluated by the scheduler; any other value (for example `NOT_EQUAL` or `GREATER_THAN`) always evaluates to false. For activity conditions use `EQUAL` with value `"true"`. | | `steps.conditions.value` | string | yes | | | `steps.next` | object | no | (recursive) | | `steps.yes` | object | no | (recursive) | | `steps.no` | object | no | (recursive) | ```json { "steps": { "_id": "START", "stepType": "START", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "next": { "stepType": "CONNECTION", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, glad to connect." } ], "next": { "stepType": "CONDITION", "identities": [], "delay": { "amount": 3, "unit": "DAYS" }, "conditions": { "field": "ACCEPTED", "operator": "EQUAL", "value": "true" }, "yes": { "stepType": "MESSAGE", "identities": [], "delay": { "amount": 1, "unit": "HOURS" }, "versions": [ { "message": "Thanks for connecting {{firstName}}!" } ] }, "no": { "stepType": "EMAIL", "identities": [], "delay": { "amount": 2, "unit": "HOURS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

I sent you an invitation on LinkedIn...

" } ], "next": { "stepType": "END_OF_CAMPAIGN", "identities": [], "delay": { "amount": 0, "unit": "DAYS" } } } } } } } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/steps" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "steps": { "_id": "START", "stepType": "START", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "next": { "stepType": "CONNECTION", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, glad to connect." } ], "next": { "stepType": "CONDITION", "identities": [], "delay": { "amount": 3, "unit": "DAYS" }, "conditions": { "field": "ACCEPTED", "operator": "EQUAL", "value": "true" }, "yes": { "stepType": "MESSAGE", "identities": [], "delay": { "amount": 1, "unit": "HOURS" }, "versions": [ { "message": "Thanks for connecting {{firstName}}!" } ] }, "no": { "stepType": "EMAIL", "identities": [], "delay": { "amount": 2, "unit": "HOURS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

I sent you an invitation on LinkedIn...

" } ], "next": { "stepType": "END_OF_CAMPAIGN", "identities": [], "delay": { "amount": 0, "unit": "DAYS" } } } } } } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/steps', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "steps": { "_id": "START", "stepType": "START", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "next": { "stepType": "CONNECTION", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, glad to connect." } ], "next": { "stepType": "CONDITION", "identities": [], "delay": { "amount": 3, "unit": "DAYS" }, "conditions": { "field": "ACCEPTED", "operator": "EQUAL", "value": "true" }, "yes": { "stepType": "MESSAGE", "identities": [], "delay": { "amount": 1, "unit": "HOURS" }, "versions": [ { "message": "Thanks for connecting {{firstName}}!" } ] }, "no": { "stepType": "EMAIL", "identities": [], "delay": { "amount": 2, "unit": "HOURS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

I sent you an invitation on LinkedIn...

" } ], "next": { "stepType": "END_OF_CAMPAIGN", "identities": [], "delay": { "amount": 0, "unit": "DAYS" } } } } } } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/steps", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "steps": { "_id": "START", "stepType": "START", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "next": { "stepType": "CONNECTION", "identities": [], "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, glad to connect." } ], "next": { "stepType": "CONDITION", "identities": [], "delay": { "amount": 3, "unit": "DAYS" }, "conditions": { "field": "ACCEPTED", "operator": "EQUAL", "value": "True" }, "yes": { "stepType": "MESSAGE", "identities": [], "delay": { "amount": 1, "unit": "HOURS" }, "versions": [ { "message": "Thanks for connecting {{firstName}}!" } ] }, "no": { "stepType": "EMAIL", "identities": [], "delay": { "amount": 2, "unit": "HOURS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

I sent you an invitation on LinkedIn...

" } ], "next": { "stepType": "END_OF_CAMPAIGN", "identities": [], "delay": { "amount": 0, "unit": "DAYS" } } } } } } }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Update campaign settings Source: https://docs.emelia.io/docs/campaigns/update-campaign-settings Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `PATCH https://api.emelia.io/advanced/campaigns/{campaignId}/settings` Merges the given settings into the campaign schedule. Not allowed while RUNNING. Rules: `start` must be before `end`; `dailyEmailLimit` must be greater than or equal to `dailyEmailAdded`; `dailyLinkedinAdded` is capped at 40 when the sequence contains a CONNECTION step (80 otherwise). Returns the updated campaign. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `settings` | object | yes | Settings to merge into the campaign schedule. `trackLinks`, `trackOpens` and `blacklistUnsub` are required by validation; `start` and `end` are optional in the validator but the handler crashes without them, so always send them. Send the full object you read with `GET /advanced/campaigns/{campaignId}` (field `schedule`). | | `settings.dailyEmailAdded` | integer | no | New contacts entering the email part of the sequence per day. Default: `35`. Range: 1 to 500. | | `settings.dailyEmailLimit` | integer | no | Maximum emails sent per day. Must be greater than or equal to `dailyEmailAdded`. Default: `100`. Range: 1 to 500. | | `settings.dailyLinkedinAdded` | integer | no | New contacts per day on LinkedIn. Real ceiling: 40 when the sequence contains a CONNECTION step, 80 otherwise. Default: `20`. Range: 1 to 80. | | `settings.dailyWhatsappAdded` | integer | no | Default: `20`. Range: 1 to 30. | | `settings.bcc` | string | no | Blind carbon copy address added to every email (empty string to disable). | | `settings.trackLinks` | boolean | yes | Required for `CLICKED` conditions and click statistics. Default: `false`. | | `settings.trackOpens` | boolean | yes | Required for `OPENED` conditions and open statistics. Default: `false`. | | `settings.timeZone` | string | no | IANA time zone, for example `Europe/Paris`. Default: `"Europe/Brussels"`. | | `settings.days` | array of integer | no | Sending days, 0 = Monday ... 6 = Sunday. Default: `[0,1,2,3,4]`. | | `settings.start` | string | yes | Default: `"08:00"`. | | `settings.end` | string | yes | Default: `"17:00"`. | | `settings.blacklistUnsub` | boolean | yes | Adds contacts who unsubscribe to your blacklist. Default: `false`. | | `settings.excludeAlreadyMessaged` | boolean | no | | | `settings.eventToStop` | array of enum: `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `LINKEDIN_REPLIED`, `MAIL_REPLIED`, `RE_REPLY_EMAIL`, `RE_REPLY_LINKEDIN`, `FOLLOWED`, `LIKED`, `SENT`, `BOUNCED`, `RE_REPLY`, `OPENED`, `UNSUBSCRIBED`, `CLICKED`, `WHATSAPP_MESSAGE_SENT`, `WHATSAPP_REPLIED`, `TASK_COMPLETED`, `TASK_FAILED`, `FAILED_ACTION`, `EXCLUDED` | no | A contact leaves the sequence as soon as one of these events happens. New campaigns start with `["MAIL_REPLIED", "LINKEDIN_REPLIED", "WHATSAPP_REPLIED"]`. Bounced and unsubscribed contacts always stop. | | `settings.alwaysOn` | boolean | no | Keeps the campaign running when all contacts are done, so that contacts added later are processed. Default: `false`. | | `settings.ignoreAutoReplies` | boolean | no | Default: `false`. | | `settings.name` | string | no | Optional new name. | ```json { "settings": { "trackLinks": false, "trackOpens": false, "blacklistUnsub": true, "dailyEmailAdded": 30, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "08:30", "end": "18:00", "eventToStop": [ "MAIL_REPLIED", "LINKEDIN_REPLIED" ] } } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/settings" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "settings": { "trackLinks": false, "trackOpens": false, "blacklistUnsub": true, "dailyEmailAdded": 30, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "08:30", "end": "18:00", "eventToStop": [ "MAIL_REPLIED", "LINKEDIN_REPLIED" ] } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/settings', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "settings": { "trackLinks": false, "trackOpens": false, "blacklistUnsub": true, "dailyEmailAdded": 30, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "08:30", "end": "18:00", "eventToStop": [ "MAIL_REPLIED", "LINKEDIN_REPLIED" ] } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/settings", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "settings": { "trackLinks": False, "trackOpens": False, "blacklistUnsub": True, "dailyEmailAdded": 30, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "08:30", "end": "18:00", "eventToStop": [ "MAIL_REPLIED", "LINKEDIN_REPLIED" ] } }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaign` | object | no | Full multichannel campaign as stored (internal fields such as `__v`, `hubspotIntegration`, `pipedriveIntegration` and `zapierData` are returned too). | | `campaign._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.tagIds` | array of string | no | | | `campaign.identities` | array of object | no | | | `campaign.schedule` | object | no | Sending settings of a multichannel campaign. Defaults of a new campaign are shown. | | `campaign.steps` | object | no | One node of the sequence tree. The root is always a `START` step. Each node points to the next one with `next`; a `CONDITION` node branches with `yes` and `no` (a missing branch ends the sequence for the contacts that fall into it). `END_OF_CAMPAIGN` stops the sequence. EMAIL versions are A/B variants (one version per contact); MESSAGE and WHATSAPP versions are successive messages sent in order. A MESSAGE step placed after a CONNECTION step waits for the invitation to be accepted. | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | Webhooks registered on this campaign. | | `campaign.hubspotIntegration` | object | no | | | `campaign.pipedriveIntegration` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | | `campaign.__v` | integer | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "tagIds": [ "6650f1a2b839d80011c5a0f1" ], "identities": [ { "_id": "6650f1a2b839d80011c5a0f1", "userId": "6650f1a2b839d80011c5a0f1", "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0f1", "linkedin": "6650f1a2b839d80011c5a0f1", "whatsapp": "6650f1a2b839d80011c5a0f1" } ], "schedule": { "dailyEmailAdded": 35, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "dailyWhatsappAdded": 20, "bcc": "", "trackLinks": false, "trackOpens": false, "timeZone": "Europe/Brussels", "days": [ 0, 1, 2, 3, 4 ], "start": "08:00", "end": "17:00", "blacklistUnsub": false, "excludeAlreadyMessaged": true, "eventToStop": [ "VISITED" ], "alwaysOn": false, "ignoreAutoReplies": false }, "steps": { "_id": "6650f1a2b839d800738c8e79", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800c4ad78e1" ], "versions": [ { "_id": "6650f1a2b839d80061ffa284", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [ { "mediaType": "MEDIA" } ] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800db5ebfa8", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800e60a709c" ], "versions": [ { "_id": "6650f1a2b839d80069bcea47", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8006f3f4cf7", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8002afcd407" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800be025a62", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800d66b0e86", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8007d4f87d0", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800ac9b82b5", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8001b26d55d" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80044576ddc", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8001d721aa4", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800de92ba26", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d80025f91939", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800a41d81a1" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8003de932e8", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800ff6e8d48", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8005553616a", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "yes": { "_id": "6650f1a2b839d8006c228008", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800c32d0efc" ], "versions": [ { "_id": "6650f1a2b839d8006e1fe027", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800761a0cd7", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8000aec6567" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800447d29c2", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8002dfc5ce6", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800e1f3ffb0", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800f9659915", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80084b3603d" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80008e9d5bc", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d80004619884", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8001b596a86", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d8009b912d19", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800c3847301" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800a52c6ec8", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800d62ec0a8", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8005d65cfca", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "no": { "_id": "6650f1a2b839d8005817272a", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800a8ab4b96" ], "versions": [ { "_id": "6650f1a2b839d80013f3f339", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8005fa8071d", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800d7d67e45" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80095424704", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8005274960c", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800a24d236e", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d8007a5b5c0f", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800275b907f" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8008fd0d3ba", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8009543f45e", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800aeb4b288", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d800269b91e3", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800341e3fb3" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8007b27d3c6", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800cad2b9e2", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80002b6d29c", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } } }, "recipients": { "lists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "excludedLists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "processing": true }, "zapierData": [ { "_id": "6650f1a2b839d80011c5a0f1", "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "hubspotIntegration": {}, "pipedriveIntegration": {}, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Set the sender identities Source: https://docs.emelia.io/docs/campaigns/set-sender-identities Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `PATCH https://api.emelia.io/advanced/campaigns/{campaignId}/identities` Replaces the identities (sending accounts) of the campaign. Not allowed while RUNNING. For each step Emelia picks automatically, among these identities, the least used one that has an account on the channel. Rules checked at start: every identity needs an email provider if the sequence has an email step; with a LinkedIn step either use a single LinkedIn account or attach one to every identity. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `identities` | array of any | yes | | | `identities[].name` | string | yes | | | `identities[].email` | string | no | Email provider id (`_id` from `GET /email-providers`). | | `identities[].linkedin` | string | no | LinkedIn account id. | | `identities[].whatsapp` | string | no | WhatsApp account id. | ```json { "identities": [ { "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0aa", "linkedin": "6650f1a2b839d80011c5a0bb" } ] } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/identities" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "identities": [ { "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0aa", "linkedin": "6650f1a2b839d80011c5a0bb" } ] }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/identities', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "identities": [ { "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0aa", "linkedin": "6650f1a2b839d80011c5a0bb" } ] }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/identities", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "identities": [ { "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0aa", "linkedin": "6650f1a2b839d80011c5a0bb" } ] }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaign` | object | no | Full multichannel campaign as stored (internal fields such as `__v`, `hubspotIntegration`, `pipedriveIntegration` and `zapierData` are returned too). | | `campaign._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.tagIds` | array of string | no | | | `campaign.identities` | array of object | no | | | `campaign.schedule` | object | no | Sending settings of a multichannel campaign. Defaults of a new campaign are shown. | | `campaign.steps` | object | no | One node of the sequence tree. The root is always a `START` step. Each node points to the next one with `next`; a `CONDITION` node branches with `yes` and `no` (a missing branch ends the sequence for the contacts that fall into it). `END_OF_CAMPAIGN` stops the sequence. EMAIL versions are A/B variants (one version per contact); MESSAGE and WHATSAPP versions are successive messages sent in order. A MESSAGE step placed after a CONNECTION step waits for the invitation to be accepted. | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | Webhooks registered on this campaign. | | `campaign.hubspotIntegration` | object | no | | | `campaign.pipedriveIntegration` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | | `campaign.__v` | integer | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "tagIds": [ "6650f1a2b839d80011c5a0f1" ], "identities": [ { "_id": "6650f1a2b839d80011c5a0f1", "userId": "6650f1a2b839d80011c5a0f1", "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0f1", "linkedin": "6650f1a2b839d80011c5a0f1", "whatsapp": "6650f1a2b839d80011c5a0f1" } ], "schedule": { "dailyEmailAdded": 35, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "dailyWhatsappAdded": 20, "bcc": "", "trackLinks": false, "trackOpens": false, "timeZone": "Europe/Brussels", "days": [ 0, 1, 2, 3, 4 ], "start": "08:00", "end": "17:00", "blacklistUnsub": false, "excludeAlreadyMessaged": true, "eventToStop": [ "VISITED" ], "alwaysOn": false, "ignoreAutoReplies": false }, "steps": { "_id": "6650f1a2b839d800a4e02834", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80044baeb18" ], "versions": [ { "_id": "6650f1a2b839d80013570f83", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [ { "mediaType": "MEDIA" } ] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800a25c662b", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8007164273b" ], "versions": [ { "_id": "6650f1a2b839d800e3cff90e", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8000c8b40ee", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800c97cfdea" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8000e5ac9e9", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8005883bad3", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80061d63f27", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800e67c4eaa", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800738d2716" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800a9299d9d", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8009dcb7b8f", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80015dca463", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d8003f5b7624", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80031d36b08" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8004f4341bb", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800e426b271", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80045c41fd5", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "yes": { "_id": "6650f1a2b839d8006176ac81", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800688e2e89" ], "versions": [ { "_id": "6650f1a2b839d8003873fb5c", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800f6c87010", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800fa504e04" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8001e6fbabf", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8001acbc61d", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800d0a9c1e1", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800c23685b0", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800606da324" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800d934ec5f", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800dcd064bd", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800f5c16001", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d800950dae32", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800711678de" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800d24726a5", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800895e1257", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800590ca2cb", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "no": { "_id": "6650f1a2b839d8006b649885", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800d2be6fad" ], "versions": [ { "_id": "6650f1a2b839d800c7040c20", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8003c66838c", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800223b38d0" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80041903763", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800d9b8ce99", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800fdcd421d", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d8008390e2f4", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8007f9038d8" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800caf55a6b", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8001f7d0ac1", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80009d680c5", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d8006e1f3416", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80068072332" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800aa7d5d31", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8006e15b9fb", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80076a1078f", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } } }, "recipients": { "lists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "excludedLists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "processing": true }, "zapierData": [ { "_id": "6650f1a2b839d80011c5a0f1", "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "hubspotIntegration": {}, "pipedriveIntegration": {}, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Set the contact lists of a campaign Source: https://docs.emelia.io/docs/campaigns/set-contact-lists Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `PATCH https://api.emelia.io/advanced/campaigns/{campaignId}/recipients` Sets the FULL selection of lists (replace semantics): the campaign contacts become the contacts of `lists`, minus any contact whose email or LinkedIn URL appears in one of `excludedLists`. When `excludedLists` is omitted the current exclusion lists are kept. Allowed on a running campaign. A campaign cannot be started with more than 30,000 recipients. Create and fill lists with the Lists endpoints (`/lists/...`). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `lists` | array of string | yes | Ids of the lists to use. | | `excludedLists` | array of string | no | Ids of exclusion lists. | ```json { "lists": [ "6650f1a2b839d80011c5b111" ], "excludedLists": [ "6650f1a2b839d80011c5b222" ] } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/recipients" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lists": [ "6650f1a2b839d80011c5b111" ], "excludedLists": [ "6650f1a2b839d80011c5b222" ] }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/recipients', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "lists": [ "6650f1a2b839d80011c5b111" ], "excludedLists": [ "6650f1a2b839d80011c5b222" ] }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/recipients", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "lists": [ "6650f1a2b839d80011c5b111" ], "excludedLists": [ "6650f1a2b839d80011c5b222" ] }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaign` | object | no | Full multichannel campaign as stored (internal fields such as `__v`, `hubspotIntegration`, `pipedriveIntegration` and `zapierData` are returned too). | | `campaign._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.tagIds` | array of string | no | | | `campaign.identities` | array of object | no | | | `campaign.schedule` | object | no | Sending settings of a multichannel campaign. Defaults of a new campaign are shown. | | `campaign.steps` | object | no | One node of the sequence tree. The root is always a `START` step. Each node points to the next one with `next`; a `CONDITION` node branches with `yes` and `no` (a missing branch ends the sequence for the contacts that fall into it). `END_OF_CAMPAIGN` stops the sequence. EMAIL versions are A/B variants (one version per contact); MESSAGE and WHATSAPP versions are successive messages sent in order. A MESSAGE step placed after a CONNECTION step waits for the invitation to be accepted. | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | Webhooks registered on this campaign. | | `campaign.hubspotIntegration` | object | no | | | `campaign.pipedriveIntegration` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | | `campaign.__v` | integer | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "tagIds": [ "6650f1a2b839d80011c5a0f1" ], "identities": [ { "_id": "6650f1a2b839d80011c5a0f1", "userId": "6650f1a2b839d80011c5a0f1", "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0f1", "linkedin": "6650f1a2b839d80011c5a0f1", "whatsapp": "6650f1a2b839d80011c5a0f1" } ], "schedule": { "dailyEmailAdded": 35, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "dailyWhatsappAdded": 20, "bcc": "", "trackLinks": false, "trackOpens": false, "timeZone": "Europe/Brussels", "days": [ 0, 1, 2, 3, 4 ], "start": "08:00", "end": "17:00", "blacklistUnsub": false, "excludeAlreadyMessaged": true, "eventToStop": [ "VISITED" ], "alwaysOn": false, "ignoreAutoReplies": false }, "steps": { "_id": "6650f1a2b839d8005f79a1ea", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80019b7c656" ], "versions": [ { "_id": "6650f1a2b839d800d98bd779", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [ { "mediaType": "MEDIA" } ] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8004eeead5d", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800cbdb9385" ], "versions": [ { "_id": "6650f1a2b839d80086508ae8", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800dc53f8c4", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800c69323a8" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8008ab805db", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d80007185491", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800c17a3475", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800bcf039cc", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8004ffc2d10" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8007ca5dd23", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800ea5bb359", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80028be99dd", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d800abb3302e", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8004274892a" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800b9d920a9", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800ff6e1d93", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8003b8a62e7", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "yes": { "_id": "6650f1a2b839d8007e35934f", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80093c902bf" ], "versions": [ { "_id": "6650f1a2b839d800ded77632", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8007634f47a", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8000eebcc26" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80028e8fb4d", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8005ccb575f", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800dad59cd3", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800bd136f1e", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80011e1379a" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8002628a479", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d80098753403", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80009f7ed37", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d800ffb34748", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800b0d9973c" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800eafa6e97", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8006ef115d5", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8002132d1d9", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "no": { "_id": "6650f1a2b839d8003d6e0823", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80006399af3" ], "versions": [ { "_id": "6650f1a2b839d800c7136ed6", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8001f6e1686", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80037fc9862" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80060464f41", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800edc8746b", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8005df9a81f", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800d995c4a2", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8009306214e" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8003846edf5", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800403ffbc7", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8000044905b", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d800e53fa85c", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80079f2d760" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80023970813", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8002fe1dea9", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800311ab70d", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } } }, "recipients": { "lists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "excludedLists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "processing": true }, "zapierData": [ { "_id": "6650f1a2b839d80011c5a0f1", "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "hubspotIntegration": {}, "pipedriveIntegration": {}, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Send a test email Source: https://docs.emelia.io/docs/campaigns/send-test-email Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/test-email` Sends one version of an EMAIL step to `email`, from one of the campaign identities, with the variables of a random contact of the campaign. Use the step `_id` and version `_id` from `GET /advanced/campaigns/{campaignId}`. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `stepId` | string | yes | | | `versionId` | string | yes | | | `email` | string (email) | yes | Recipient of the test. | ```json { "stepId": "6650f1a2b839d80011c5c001", "versionId": "6650f1a2b839d80011c5c002", "email": "john.doe@acme.com" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/test-email" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "stepId": "6650f1a2b839d80011c5c001", "versionId": "6650f1a2b839d80011c5c002", "email": "john.doe@acme.com" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/test-email', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "stepId": "6650f1a2b839d80011c5c001", "versionId": "6650f1a2b839d80011c5c002", "email": "john.doe@acme.com" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/test-email", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "stepId": "6650f1a2b839d80011c5c001", "versionId": "6650f1a2b839d80011c5c002", "email": "john.doe@acme.com" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request or sending failure. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Start a campaign Source: https://docs.emelia.io/docs/campaigns/start-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/start` Starts (or resumes) the campaign. Requires an active subscription. All start checks are run and the first failing one is returned with a stable `code` (see the examples and the ErrorCode schema): identities, accounts connected, email versions with content and subject, LinkedIn daily ceiling, 1 to 30,000 recipients, at least one contact reachable on each channel used. A planned start, if any, is cancelled. Scheduling of contacts happens asynchronously after the response. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/start" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/start', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/start", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: The campaign cannot be started. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Schedule or cancel a planned start Source: https://docs.emelia.io/docs/campaigns/schedule-planned-start Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `PATCH https://api.emelia.io/advanced/campaigns/{campaignId}/planned-start` Schedules an automatic start at `plannedStart` (ISO 8601 date in the future). The campaign must pass the start checks at scheduling time (same errors as `POST .../start`). Send `null` (or an empty value) to cancel a planned start. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `plannedStart` | string or null (date-time) | no | | ```json { "plannedStart": "2026-11-03T08:00:00.000Z" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/planned-start" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "plannedStart": "2026-11-03T08:00:00.000Z" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/planned-start', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "plannedStart": "2026-11-03T08:00:00.000Z" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/planned-start", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "plannedStart": "2026-11-03T08:00:00.000Z" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `plannedStart` | string or null (date-time) | no | | ```json { "success": true, "plannedStart": "2026-11-03T08:00:00.000Z" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Pause a campaign Source: https://docs.emelia.io/docs/campaigns/pause-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/pause` Pauses a RUNNING campaign: its scheduled actions, pending conditions and pending manual tasks are deleted. They are created again when the campaign is restarted and the contacts reach those steps again. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/pause" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/pause', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/pause", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Mark a campaign as finished Source: https://docs.emelia.io/docs/campaigns/mark-campaign-as-finished Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/finish` Moves a RUNNING or PAUSED campaign to FINISHED and deletes its scheduled actions and pending conditions. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/finish" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/finish', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/finish", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Reactivate a finished campaign Source: https://docs.emelia.io/docs/campaigns/reactivate-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/reactivate` Moves a FINISHED campaign back to PAUSED so that it can be edited and started again. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/reactivate" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/reactivate', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/reactivate", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Rename a campaign Source: https://docs.emelia.io/docs/campaigns/rename-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `PATCH https://api.emelia.io/advanced/campaigns/{campaignId}/name` Renames the campaign. Not allowed while the campaign is RUNNING. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | | ```json { "name": "Acme Q4 outreach v2" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/name" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Q4 outreach v2" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/name', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Acme Q4 outreach v2" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/name", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Acme Q4 outreach v2" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaign` | object | no | Full multichannel campaign as stored (internal fields such as `__v`, `hubspotIntegration`, `pipedriveIntegration` and `zapierData` are returned too). | | `campaign._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.tagIds` | array of string | no | | | `campaign.identities` | array of object | no | | | `campaign.schedule` | object | no | Sending settings of a multichannel campaign. Defaults of a new campaign are shown. | | `campaign.steps` | object | no | One node of the sequence tree. The root is always a `START` step. Each node points to the next one with `next`; a `CONDITION` node branches with `yes` and `no` (a missing branch ends the sequence for the contacts that fall into it). `END_OF_CAMPAIGN` stops the sequence. EMAIL versions are A/B variants (one version per contact); MESSAGE and WHATSAPP versions are successive messages sent in order. A MESSAGE step placed after a CONNECTION step waits for the invitation to be accepted. | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | Webhooks registered on this campaign. | | `campaign.hubspotIntegration` | object | no | | | `campaign.pipedriveIntegration` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | | `campaign.__v` | integer | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "tagIds": [ "6650f1a2b839d80011c5a0f1" ], "identities": [ { "_id": "6650f1a2b839d80011c5a0f1", "userId": "6650f1a2b839d80011c5a0f1", "name": "Marie Martin", "email": "6650f1a2b839d80011c5a0f1", "linkedin": "6650f1a2b839d80011c5a0f1", "whatsapp": "6650f1a2b839d80011c5a0f1" } ], "schedule": { "dailyEmailAdded": 35, "dailyEmailLimit": 100, "dailyLinkedinAdded": 20, "dailyWhatsappAdded": 20, "bcc": "", "trackLinks": false, "trackOpens": false, "timeZone": "Europe/Brussels", "days": [ 0, 1, 2, 3, 4 ], "start": "08:00", "end": "17:00", "blacklistUnsub": false, "excludeAlreadyMessaged": true, "eventToStop": [ "VISITED" ], "alwaysOn": false, "ignoreAutoReplies": false }, "steps": { "_id": "6650f1a2b839d800498e9dff", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800271af1ef" ], "versions": [ { "_id": "6650f1a2b839d800d471e242", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [ { "mediaType": "MEDIA" } ] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80075fdd3ca", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800b2167636" ], "versions": [ { "_id": "6650f1a2b839d80048cdb6d9", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800a4dfb53d", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8001c33f465" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800d76b4b24", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800e2281dac", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800fa90870e", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d80008d9a42f", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800eb3ca81f" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d80061a9645a", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800dcd057fe", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800d26cc328", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d8000a64af83", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800bd846cd3" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8001f2c2de6", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8001c87cb02", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8003244233c", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "yes": { "_id": "6650f1a2b839d800e7933ace", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8003e92faca" ], "versions": [ { "_id": "6650f1a2b839d8002ce293dd", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8002f2974c9", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d80009d3fdb1" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800842890d8", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8002ee6ba18", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d8009335aa9a", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d800bf8452b3", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8000842a783" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800bc920876", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800345524b2", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800641f460c", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d800bd5a3307", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800756d2097" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800c1089d12", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800909fd0d6", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800d3c6efc0", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } }, "no": { "_id": "6650f1a2b839d800aff80158", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800e3061a0c" ], "versions": [ { "_id": "6650f1a2b839d800ac97aed7", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [] } ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800cc57a087", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d8002ae1d217" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d800e3f18592", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800bf669656", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800cc968440", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "yes": { "_id": "6650f1a2b839d8008b5d86a5", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800f09d444d" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8007df2c9ac", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d8000b224f94", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d80018f4e036", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } }, "no": { "_id": "6650f1a2b839d800d99d6d49", "stepType": "START", "delay": { "amount": 1, "unit": "MINUTES" }, "identities": [ "6650f1a2b839d800e5e64231" ], "versions": [ {} ], "cc": "", "conditions": { "field": "MAIL_REPLIED", "operator": "EQUAL" }, "next": { "_id": "6650f1a2b839d8009fea5a58", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "yes": { "_id": "6650f1a2b839d800c0c1ba98", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } }, "no": { "_id": "6650f1a2b839d800dcf0f81a", "stepType": "START", "delay": { "unit": "MINUTES" }, "identities": [], "versions": [], "cc": "", "conditions": { "operator": "EQUAL" }, "next": { "stepType": "START" }, "yes": { "stepType": "START" }, "no": { "stepType": "START" } } } } }, "recipients": { "lists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "excludedLists": [ { "_id": "6650f1a2b839d80011c5a0f1", "name": "Q4 outreach: SaaS founders", "contactCount": 1 } ], "processing": true }, "zapierData": [ { "_id": "6650f1a2b839d80011c5a0f1", "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "hubspotIntegration": {}, "pipedriveIntegration": {}, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Duplicate a campaign Source: https://docs.emelia.io/docs/campaigns/duplicate-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/duplicate` Creates a new DRAFT campaign from an existing one. Choose what to copy. Copied contacts and lists are shared references (not copies). The new name must be unique. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | | | `copySettings` | boolean | no | Default: `false`. | | `copySteps` | boolean | no | Default: `false`. | | `copyContacts` | boolean | no | Default: `false`. | | `copyIdentities` | boolean | no | Default: `false`. | ```json { "name": "Acme Q4 outreach (copy)", "copySettings": true, "copySteps": true, "copyContacts": false, "copyIdentities": true } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/duplicate" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme Q4 outreach (copy)", "copySettings": true, "copySteps": true, "copyContacts": false, "copyIdentities": true }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/duplicate', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Acme Q4 outreach (copy)", "copySettings": true, "copySteps": true, "copyContacts": false, "copyIdentities": true }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/duplicate", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Acme Q4 outreach (copy)", "copySettings": True, "copySteps": True, "copyContacts": False, "copyIdentities": True }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `campaignId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "campaignId": "6650f1a2b839d80011c5a0f2" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Archive a campaign Source: https://docs.emelia.io/docs/campaigns/archive-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/archive` Sets the status to ARCHIVED (hidden from `GET /advanced/campaigns`). A running campaign must be paused first. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/archive" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/archive', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/archive", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Delete a campaign Source: https://docs.emelia.io/docs/campaigns/delete-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `DELETE https://api.emelia.io/advanced/campaigns/{campaignId}` Deletes the campaign. A running campaign must be paused first. Irreversible. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # 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: `. ## 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" } ``` --- # Add a contact to a campaign Source: https://docs.emelia.io/docs/campaigns/add-contact-to-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaign/contacts` Adds ONE contact to a multichannel campaign. The contact is stored in a list named " contacts from API" (created if needed) attached to the campaign. If the campaign is FINISHED it is started again automatically (all start checks apply); if it is RUNNING the contact is scheduled immediately. A contact already present in that list (same email or LinkedIn URL) is rejected. For many contacts, use the Lists endpoints and `PATCH .../recipients` instead. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | MongoDB ObjectId: 24 hexadecimal characters. | | `contact` | object | yes | Contact to add. At least one of `email` or `linkedinUrlProfile`. Only `email`, `linkedinUrlProfile`, `phone`, `firstName` and `lastName` are stored as native fields; every other key becomes a custom variable usable as `{{key}}` in messages. ALL VALUES MUST BE STRINGS: a number, an object or an array makes the whole request fail with a 400 error. Empty strings are ignored. | | `contact.email` | string (email) | no | | | `contact.linkedinUrlProfile` | string | no | https://www.linkedin.com/in/. Sales Navigator lead URLs (`/sales/lead/` or `/sales/people/`) are converted when possible; other URLs are rejected. | | `contact.firstName` | string | no | | | `contact.lastName` | string | no | | | `contact.phone` | string | no | | ```json { "id": "6650f1a2b839d80011c5a0f1", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/johndoe", "companyName": "Acme", "jobTitle": "Head of Sales" } } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaign/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "6650f1a2b839d80011c5a0f1", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/johndoe", "companyName": "Acme", "jobTitle": "Head of Sales" } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaign/contacts', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": "6650f1a2b839d80011c5a0f1", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/johndoe", "companyName": "Acme", "jobTitle": "Head of Sales" } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "6650f1a2b839d80011c5a0f1", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/johndoe", "companyName": "Acme", "jobTitle": "Head of Sales" } }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `contactId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "contactId": "6650f1a2b839d80011c5d001" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Remove a contact from a campaign Source: https://docs.emelia.io/docs/campaigns/remove-contact-from-campaign Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `DELETE https://api.emelia.io/advanced/campaign/contacts` Removes one contact from the campaign recipients and deletes its activities in this campaign (in the background). The contact itself and its lists are kept. The parameters are sent in the JSON BODY of the DELETE request. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | MongoDB ObjectId: 24 hexadecimal characters. | | `contactId` | string | yes | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "id": "6650f1a2b839d80011c5a0f1", "contactId": "6650f1a2b839d80011c5d001" } ``` ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/advanced/campaign/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "6650f1a2b839d80011c5a0f1", "contactId": "6650f1a2b839d80011c5d001" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaign/contacts', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": "6650f1a2b839d80011c5a0f1", "contactId": "6650f1a2b839d80011c5d001" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/advanced/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "6650f1a2b839d80011c5a0f1", "contactId": "6650f1a2b839d80011c5d001" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 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" } ``` --- # Set a field on a campaign contact Source: https://docs.emelia.io/docs/campaigns/set-contact-field Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `PATCH https://api.emelia.io/advanced/contacts` Sets one field of a contact that belongs to the campaign, found by `email` OR `linkedinUrlProfile` (exactly one of them). Spaces are removed from `fieldName`. If `fieldName` is a native field of the contact (for example `firstName`, `jobTitle`, `companyName`) it is overwritten, otherwise a custom variable is created or updated. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | MongoDB ObjectId: 24 hexadecimal characters. | | `email` | string | no | | | `linkedinUrlProfile` | string | no | | | `fieldName` | string | yes | | | `fieldValue` | string | yes | | ```json { "campaignId": "6650f1a2b839d80011c5a0f1", "email": "john.doe@acme.com", "fieldName": "meetingLink", "fieldValue": "https://cal.example.com/john" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/advanced/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "campaignId": "6650f1a2b839d80011c5a0f1", "email": "john.doe@acme.com", "fieldName": "meetingLink", "fieldValue": "https://cal.example.com/john" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/contacts', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "campaignId": "6650f1a2b839d80011c5a0f1", "email": "john.doe@acme.com", "fieldName": "meetingLink", "fieldValue": "https://cal.example.com/john" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/advanced/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "campaignId": "6650f1a2b839d80011c5a0f1", "email": "john.doe@acme.com", "fieldName": "meetingLink", "fieldValue": "https://cal.example.com/john" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # List pending manual tasks Source: https://docs.emelia.io/docs/campaigns/list-pending-manual-tasks Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `GET https://api.emelia.io/advanced/campaigns/{campaignId}/manual-tasks` Pending TASK steps of the campaign (contacts waiting for you to do something by hand), oldest first. Pausing the campaign deletes pending tasks; they are created again after the next start, when contacts reach the task step again. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `stepId` | string | no | Only the tasks of this TASK step. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/manual-tasks" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/manual-tasks', { 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/campaigns/6650f1a2b839d80011c5a0f1/manual-tasks", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `tasks` | array of object | no | | | `tasks[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `tasks[].firstName` | string | no | | | `tasks[].lastName` | string | no | | | `tasks[].email` | string | no | | | `tasks[].linkedinUrlProfile` | string | no | | | `tasks[].status` | string | no | | | `tasks[].taskId` | string | no | Id of the TASK step. Send it back with `POST .../manual-tasks/status`. | | `tasks[].taskName` | string | no | `versions[0].subject` of the TASK step. | | `tasks[].date` | string (date-time) | no | When the task became pending. | | `tasks[].multipleChoice` | boolean | no | True when the task is followed by a `TASK_COMPLETED` condition (ok and ko lead to different branches). | ```json { "success": true, "tasks": [ { "_id": "6650f1a2b839d80011c5a0f1", "firstName": "Claire", "lastName": "Fontaine", "email": "claire.fontaine@nexora.io", "linkedinUrlProfile": "https://www.linkedin.com/in/claire-fontaine", "taskId": "6650f1a2b839d800b174ee18", "taskName": "Call Claire", "date": "2026-10-06T09:30:00.000Z", "multipleChoice": true } ] } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Complete a manual task Source: https://docs.emelia.io/docs/campaigns/complete-manual-task Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/campaigns/{campaignId}/manual-tasks/status` Marks the task of one contact as done (`ok`, event TASK_COMPLETED) or failed (`ko`, event TASK_FAILED) and moves the contact to the next step. A task can only be completed once. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `contactId` | string | yes | MongoDB ObjectId: 24 hexadecimal characters. | | `taskId` | string | yes | `taskId` from the list of tasks. | | `status` | enum: `ok`, `ko` | yes | | ```json { "contactId": "6650f1a2b839d80011c5d001", "taskId": "6650f1a2b839d80011c5c010", "status": "ok" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/manual-tasks/status" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contactId": "6650f1a2b839d80011c5d001", "taskId": "6650f1a2b839d80011c5c010", "status": "ok" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/manual-tasks/status', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "contactId": "6650f1a2b839d80011c5d001", "taskId": "6650f1a2b839d80011c5c010", "status": "ok" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/manual-tasks/status", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "contactId": "6650f1a2b839d80011c5d001", "taskId": "6650f1a2b839d80011c5c010", "status": "ok" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Get per step progress Source: https://docs.emelia.io/docs/campaigns/get-per-step-progress Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `GET https://api.emelia.io/advanced/campaigns/{campaignId}/progress` For each step id: how many contacts already passed the step (`completed`) and how many are currently waiting at it (`waiting`). Works on paused campaigns. Returns `{}` for a DRAFT or unknown campaign. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/progress" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/progress', { 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/campaigns/6650f1a2b839d80011c5a0f1/progress", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `progress` | object | no | | ```json { "success": true, "progress": { "6650f1a2b839d80011c5c001": { "completed": 120, "waiting": 35 } } } ``` ### 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" } ``` --- # List the contacts of a step Source: https://docs.emelia.io/docs/campaigns/list-contacts-of-step Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `GET https://api.emelia.io/advanced/campaigns/{campaignId}/progress/contacts` Contacts that passed (`completed`) or wait at (`waiting`) a given step, newest first, 20 per page. Here `page` is ONE based. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `stepId` | string | yes | | | `status` | enum: `all`, `completed`, `waiting` | no | Default: `"all"`. | | `page` | integer | no | Default: `1`. Range: 1 to .... | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/progress/contacts?stepId=65f1a2b3c4d5e6f7ec005b62" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/progress/contacts?stepId=65f1a2b3c4d5e6f7ec005b62', { 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/campaigns/6650f1a2b839d80011c5a0f1/progress/contacts?stepId=65f1a2b3c4d5e6f7ec005b62", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `contacts` | array of object | no | | | `contacts[].contactId` | string | no | | | `contacts[].firstName` | string | no | | | `contacts[].lastName` | string | no | | | `contacts[].email` | string | no | | | `contacts[].linkedinUrlProfile` | string | no | | | `contacts[].status` | enum: `completed`, `waiting` | no | | | `contacts[].date` | string or null (date-time) | no | | | `total` | integer | no | | | `totalPages` | integer | no | | | `completedTotal` | integer | no | | | `waitingTotal` | integer | no | | ```json { "success": true, "contacts": [ { "contactId": "6650f1a2b839d8001ee1c24d", "firstName": "Claire", "lastName": "Fontaine", "email": "claire.fontaine@nexora.io", "linkedinUrlProfile": "https://www.linkedin.com/in/claire-fontaine", "status": "completed", "date": "2026-10-06T09:30:00.000Z" } ], "total": 250, "totalPages": 13, "completedTotal": 180, "waitingTotal": 70 } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Export campaign data as CSV Source: https://docs.emelia.io/docs/campaigns/export-as-csv Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `GET https://api.emelia.io/advanced/campaigns/{campaignId}/export` Returns a CSV file (UTF-8 with BOM, `Content-Disposition: attachment`). `contacts`: one line per contact with its custom fields, activities and replied flag; `activities`: activity log; `replies`: replies. Optional date range on activities. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `type` | enum: `contacts`, `activities`, `replies` | yes | | | `start` | string (date-time) | no | | | `end` | string (date-time) | no | | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/export?type=contacts" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/export?type=contacts', { 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/campaigns/6650f1a2b839d80011c5a0f1/export?type=contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: CSV file. Content type: `text/csv`. ### 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" } ``` --- # Get campaign statistics Source: https://docs.emelia.io/docs/campaigns/get-campaign-statistics Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `GET https://api.emelia.io/stats/advanced` Global and per step statistics of a multichannel campaign. Computed on demand (can take several seconds) and then cached; `Cache-Control: max-age=300` is set when `detailed` is not `true`. Errors are unusual here: an unknown campaign or ANY error (including an invalid parameter) returns 401, and a failure of the statistics engine can return HTTP 200 with `{"error": "Error"}`. For a few accounts a 503 `STATS_COMPUTING` with `Retry-After: 30` is returned while a computation is in progress. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | MongoDB ObjectId: 24 hexadecimal characters. | | `detailed` | boolean | no | For multichannel campaigns the shape is always `{global, steps}`. | | `start` | string (date-time) | no | | | `end` | string (date-time) | no | | | `providerId` | string | no | Restrict to one email provider. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/stats/advanced?campaignId=string" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/stats/advanced?campaignId=string', { 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/stats/advanced?campaignId=string", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. ```json { "global": { "contacts": 250, "contacted": 240, "replied": 18, "visited": 120, "invited": 100, "accepted": 41, "message_sent": 38, "inmail_sent": 0, "linkedin_replied": 9, "mail_replied": 9, "liked": 30, "sent": 240, "opened": 96, "clicked": 21, "bounced": 3, "unsubscribed": 2, "progress_percent": 96, "accepted_percent": 41, "opened_percent": 40, "clicked_percent": 8.75, "linkedin_replied_percent": 9, "mail_replied_percent": 3.75, "bounced_percent": 1.25, "unsubscribed_percent": 0.83 }, "steps": [ [ { "stepType": "EMAIL" } ] ] } ``` ### 401: Unknown campaign, invalid parameter or any other error (all mapped to 401). | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` ### 503: Statistics already being computed (only for some accounts). | 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": "Statistics are being computed, please try again in a few moments", "code": "STATS_COMPUTING" } ``` --- # Upload message attachments Source: https://docs.emelia.io/docs/campaigns/upload-message-attachments Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/attachments` Uploads 1 to 3 files (20 MB maximum each) and returns their URLs, to reference in `versions[].attachments` of EMAIL, MESSAGE or WHATSAPP steps. Files are stored as is with a PUBLIC URL: anyone with the URL can download them. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `multipart/form-data`, required. | Field | Type | Required | Description | |---|---|---|---| | `attachments` | array of string (binary) | yes | Max 3 items. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/attachments" \ -H "Authorization: $EMELIA_API_KEY" \ -F "attachments=@./file.pdf" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/attachments', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, body: form, // a FormData with the fields of the request body }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/attachments", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, files=files, # the fields of the request body ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `attachments` | array of object | no | | | `attachments[].url` | string (uri) | no | | | `attachments[].name` | string | no | | ```json { "success": true, "attachments": [ { "url": "https://eml-public.s3.eu-west-3.amazonaws.com/linkedin/attachments/6650f1a2b839d80011c5a001/1760000000000_ab12cd34ef56ab78_brochure.pdf", "name": "brochure.pdf" } ] } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Upload a LinkedIn voice note Source: https://docs.emelia.io/docs/campaigns/upload-linkedin-voice-note Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md) `POST https://api.emelia.io/advanced/linkedin/audio` Converts an audio file (webm expected, 10 MB maximum, 5 minutes maximum) to m4a and returns a PUBLIC URL plus its duration IN MILLISECONDS, to use as an attachment with `mediaType: "AUDIO"` in a MESSAGE step. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `multipart/form-data`, required. | Field | Type | Required | Description | |---|---|---|---| | `audio` | string (binary) | yes | | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/advanced/linkedin/audio" \ -H "Authorization: $EMELIA_API_KEY" \ -F "audio=" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/linkedin/audio', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, body: form, // a FormData with the fields of the request body }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/advanced/linkedin/audio", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, files=files, # the fields of the request body ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `audio` | object | no | | | `audio.duration` | integer | no | Milliseconds. | | `audio.url` | string (uri) | no | | | `audio.name` | string | no | | ```json { "success": true, "audio": { "duration": 32000, "url": "https://eml-public.s3.eu-west-3.amazonaws.com/linkedin/audio/1760000000000_voice-note.m4a", "name": "voice-note.m4a" } } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Legacy campaigns > The routes of the previous Emelia campaign system, email only or LinkedIn only. Deprecated, still working for existing integrations. Source: https://docs.emelia.io/docs/campaigns/legacy > **Deprecated.** These routes belong to the previous campaign system, where a campaign was either email only or LinkedIn only. They keep working for existing integrations, but they are not developed any more. Every new integration should use the [Campaigns](https://docs.emelia.io/docs/campaigns.md) routes (`/advanced/...`): every campaign of the current app is multichannel. | Legacy system | Routes | Replaced by | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) | `/emails/campaigns...`, `/emails/campaign/contacts`, `/emails/contacts`, `/emails/test`, `/stats` | [Campaigns](https://docs.emelia.io/docs/campaigns.md) with `EMAIL` steps | | [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) | `/linkedin/campaigns...`, `/linkedin/campaign/contacts`, `/stats/linkedin` | [Campaigns](https://docs.emelia.io/docs/campaigns.md) with LinkedIn steps | The legacy campaigns are separate objects: a legacy campaign id does not work on the `/advanced/...` routes, and the other way round. ## What differs from the current routes * **Envelopes.** The contact listings answer `{ "success": true, "list": [...], "count": 42 }`, with a `page` that starts at 0. * **Statistics.** `GET /stats` and `GET /stats/linkedin` answer a flat summary by default, and `{ global, steps }` with `detailed=true` (the string). An unknown campaign answers `401`, not `400`. * **Sequences.** The steps are an ordered array, not a tree, and every step after the first needs a `delay.amount` above 0. * **Intervals.** `minInterval` and `maxInterval` of the email schedule are in seconds. `days` goes from 0 (Monday) to 6 (Sunday), as everywhere. * **Errors.** Most errors are plain sentences with a `400`, like on the rest of the main API. The [blacklist](https://docs.emelia.io/docs/blacklist.md) routes also start with `/emails/` but are not legacy: they apply to every campaign. --- # Email campaigns > The routes of the previous email only campaigns. Deprecated, still working for existing integrations. Source: https://docs.emelia.io/docs/campaigns/legacy/email > **Deprecated.** These routes manage the email only campaigns of the previous system. They keep working, but new integrations should create [Campaigns](https://docs.emelia.io/docs/campaigns.md) with `EMAIL` steps: same sending engine, plus LinkedIn, conditions and tracking per step. ## What you can still do | Need | Route | | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | List or read the legacy email campaigns | [List campaigns](https://docs.emelia.io/docs/campaigns/legacy/email/list-campaigns.md), [Get a campaign](https://docs.emelia.io/docs/campaigns/legacy/email/get-campaign.md) | | Feed a running legacy campaign | [Add a contact](https://docs.emelia.io/docs/campaigns/legacy/email/add-contact.md), [Add a contact to a list](https://docs.emelia.io/docs/campaigns/legacy/email/add-contact-to-list.md) | | Update one field of a contact | [Set a contact field](https://docs.emelia.io/docs/campaigns/legacy/email/set-contact-field.md) | | Read the results | [Get activities](https://docs.emelia.io/docs/campaigns/legacy/email/get-activities.md), [Get statistics](https://docs.emelia.io/docs/campaigns/legacy/email/get-statistics.md) | [`POST /emails/campaign/contacts`](https://docs.emelia.io/docs/campaigns/legacy/email/add-contact.md) **curl** ```bash curl -X POST https://api.emelia.io/emails/campaign/contacts \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id": "6650f1a2b839d80011c5a0f1", "contact": {"email": "claire.fontaine@nexora.io", "firstName": "Claire", "lastName": "Fontaine"}}' ``` **JavaScript** ```js await fetch('https://api.emelia.io/emails/campaign/contacts', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ id: '6650f1a2b839d80011c5a0f1', contact: { email: 'claire.fontaine@nexora.io', firstName: 'Claire', lastName: 'Fontaine' }, }), }); ``` **Python** ```python import os import requests requests.post( "https://api.emelia.io/emails/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "6650f1a2b839d80011c5a0f1", "contact": {"email": "claire.fontaine@nexora.io", "firstName": "Claire", "lastName": "Fontaine"}, }, ) ``` The contact goes to a list named after the campaign. Keys other than `email`, `firstName`, `lastName` and `phoneNumber` become custom variables. A finished campaign starts again, a running one schedules the contact at once, and an email already in the campaign is refused. ## Good to know * The listing of contacts pages from `page=0` and answers `{ success, list, count }`. Its `event` filter must be an array: `event[]=REPLIED`. * `PATCH .../settings/global` replaces the whole schedule. `minInterval` and `maxInterval` are in seconds, `dailyLimit` must be at least `dailyContact`. * The steps are an ordered array. Every step after the first needs `delay.amount` above 0. * `GET /stats` answers `401` for an unknown campaign, and a flat summary unless `detailed=true`. --- # List email campaign contacts (paginated) Source: https://docs.emelia.io/docs/campaigns/legacy/email/list-campaign-contacts Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `GET https://api.emelia.io/emails/campaign/contacts` Returns a page of the campaign recipients. Pagination is 0-based (page=0 is the first page). perPage defaults to 20, values below 10 are forced to 20, and values above 10000 are capped at 10000. The response envelope is { success, list, count } (NOT { contacts, total }). event filters by contact status and MUST be an array (e.g. event[]=REPLIED); a bare string is rejected (contrast with the LinkedIn variant which also accepts a string). New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Campaign ObjectId. | | `page` | string | yes | 0-based page index (sent as a string). | | `perPage` | string | no | Page size. Default 20, min 10 (else forced to 20), max 10000. | | `event` | array of enum: `SENT`, `OPENED`, `CLICKED`, `REPLIED`, `BOUNCED`, `UNSUBSCRIBED`, `INTERESTED`, `NO_ACTIVITY` | no | Filter by contact status. Must be an array. | | `query` | string | no | Full email match, or space-separated tokens matched against email/firstName/lastName. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/emails/campaign/contacts?id=65f1a2b3c4d5e6f73478765e&page=0" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaign/contacts?id=65f1a2b3c4d5e6f73478765e&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/emails/campaign/contacts?id=65f1a2b3c4d5e6f73478765e&page=0", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: One page of contacts. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `list` | array of object | yes | | | `list[]._id` | string | no | | | `list[].email` | string (email) | no | | | `list[].firstName` | string | no | | | `list[].lastName` | string | no | | | `list[].phoneNumber` | string | no | | | `list[].custom` | object | no | Custom variables. | | `list[].status` | string | no | Latest status, e.g. SENT, OPENED, CLICKED, REPLIED, BOUNCED, UNSUBSCRIBED, BLACKLISTED. '-' when none. | | `list[].mailsSent` | integer | no | | | `list[].lastContacted` | string (date-time) | no | | | `list[].lastOpen` | string (date-time) | no | | | `list[].lastReplied` | string (date-time) | no | | | `list[].interested` | string | no | Legacy field, always "-" in campaign contact listings. | | `count` | integer | yes | Total number of matching contacts. | ```json { "success": true, "count": 2, "list": [ { "_id": "5f3a1b2c4d5e6f00112233aa", "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "status": "SENT", "mailsSent": 1, "interested": "-" } ] } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Add a contact to an email campaign Source: https://docs.emelia.io/docs/campaigns/legacy/email/add-contact Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `POST https://api.emelia.io/emails/campaign/contacts` Adds one contact to the campaign (id). The contact is appended to a dedicated list named ' from Zapier/Integromat/n8n Hooks'. Any extra key in contact beyond email/firstName/lastName/phoneNumber is stored as a custom variable. Side effects: if the campaign is FINISHED it is restarted (status RUNNING); if RUNNING the contact is scheduled immediately. Fails if the email is already in the campaign. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Campaign ObjectId. | | `contact` | object | yes | | | `contact.email` | string (email) | yes | | | `contact.firstName` | string | no | | | `contact.lastName` | string | no | | | `contact.phoneNumber` | string | no | | ```json { "id": "5f3a1b2c4d5e6f0011223344", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "company": "Acme" } } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/campaign/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "5f3a1b2c4d5e6f0011223344", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "company": "Acme" } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaign/contacts', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": "5f3a1b2c4d5e6f0011223344", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "company": "Acme" } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "5f3a1b2c4d5e6f0011223344", "contact": { "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe", "company": "Acme" } }, ) print(response.json()) ``` ## Responses ### 200: Contact added. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Remove a contact from an email campaign Source: https://docs.emelia.io/docs/campaigns/legacy/email/remove-contact Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `DELETE https://api.emelia.io/emails/campaign/contacts` Removes the contact whose email matches from the campaign (id). Use id='ALL_CAMPAIGNS' to remove the email from every non-archived campaign. Note: the body field is 'email' (not 'contact'). New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Campaign ObjectId, or 'ALL_CAMPAIGNS'. | | `email` | string (email) | yes | | ```json { "id": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com" } ``` ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/emails/campaign/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaign/contacts', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/emails/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com" }, ) print(response.json()) ``` ## Responses ### 200: Contact removed (always success:true, even if no contact matched). | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Add a contact to an email list Source: https://docs.emelia.io/docs/campaigns/legacy/email/add-contact-to-list Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `POST https://api.emelia.io/emails/lists/contacts` Adds one contact to an existing contact list (id = list ObjectId). Any extra key in contact is stored as a custom variable. Fails if the email is already in the list. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Contact list ObjectId. | | `contact` | object | yes | | | `contact.email` | string (email) | yes | | | `contact.firstName` | string | no | | | `contact.lastName` | string | no | | | `contact.phoneNumber` | string | no | | ```json { "id": "5f3a1b2c4d5e6f0011223366", "contact": { "email": "john.doe@acme.com", "firstName": "John" } } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/lists/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "5f3a1b2c4d5e6f0011223366", "contact": { "email": "john.doe@acme.com", "firstName": "John" } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/lists/contacts', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": "5f3a1b2c4d5e6f0011223366", "contact": { "email": "john.doe@acme.com", "firstName": "John" } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/lists/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "5f3a1b2c4d5e6f0011223366", "contact": { "email": "john.doe@acme.com", "firstName": "John" } }, ) print(response.json()) ``` ## Responses ### 200: Contact added. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # List email campaigns Source: https://docs.emelia.io/docs/campaigns/legacy/email/list-campaigns Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `GET https://api.emelia.io/emails/campaigns` Returns non-archived email campaigns with a reduced field set (_id, name, status, createdAt, startAt, useManyProviders, providersUsed, plannedStart) and the populated provider. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/emails/campaigns" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns', { 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/emails/campaigns", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Campaigns. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaigns` | array of object | yes | | | `campaigns[]._id` | string | no | | | `campaigns[].name` | string | no | | | `campaigns[].status` | string | no | | | `campaigns[].createdAt` | string (date-time) | no | | | `campaigns[].startAt` | string (date-time) | no | | | `campaigns[].plannedStart` | string (date-time) | no | | | `campaigns[].useManyProviders` | boolean | no | | | `campaigns[].providersUsed` | array of string | no | | | `campaigns[].provider` | object or null | no | Populated email provider. | ```json { "success": true, "campaigns": [ { "_id": "6650f1a2b839d80019331cde", "name": "Q4 outreach: SaaS founders", "status": "RUNNING", "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z", "useManyProviders": true, "providersUsed": [ "6650f1a2b839d8006bc15fb2" ], "provider": {} } ] } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Create an email campaign Source: https://docs.emelia.io/docs/campaigns/legacy/email/create-campaign Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `POST https://api.emelia.io/emails/campaigns` Creates an empty email (single-channel) campaign in DRAFT status with one empty first step. name must be unique in the workspace. Returns the full created campaign document. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | | ```json { "name": "Q1 cold outreach" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/campaigns" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Q1 cold outreach" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Q1 cold outreach" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/campaigns", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Q1 cold outreach" }, ) print(response.json()) ``` ## Responses ### 200: Campaign created. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full email campaign document (as returned by create/get/update). | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.provider` | string or null | no | | | `campaign.useManyProviders` | boolean | no | | | `campaign.providersUsed` | array of string | no | | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d8002dbcafa1", "userId": "6650f1a2b839d800e9d7b619", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "provider": "6650f1a2b839d800c323fd2f", "useManyProviders": true, "providersUsed": [ "6650f1a2b839d800d6a7ea7d" ], "schedule": { "bcc": "", "dailyContact": 35, "dailyLimit": 100, "minInterval": 60, "maxInterval": 180, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "eventToStopMails": [] }, "steps": [ { "_id": "6650f1a2b839d800244e963c", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800e7acf55b", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [], "options": {} } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d800e2b43cb7" ], "processing": true }, "zapierData": [ { "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Get email campaign details Source: https://docs.emelia.io/docs/campaigns/legacy/email/get-campaign Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `GET https://api.emelia.io/emails/campaigns/{campaignId}` Returns the full campaign document (minus __v) with recipients.lists populated to { _id, name }. Errors with 'Campaign not found' when absent. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e', { 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/emails/campaigns/65f1a2b3c4d5e6f73478765e", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full email campaign document (as returned by create/get/update). | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.provider` | string or null | no | | | `campaign.useManyProviders` | boolean | no | | | `campaign.providersUsed` | array of string | no | | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d8000d605583", "userId": "6650f1a2b839d8002a1aea4b", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "provider": "6650f1a2b839d800996c5c21", "useManyProviders": true, "providersUsed": [ "6650f1a2b839d800edcfb6df" ], "schedule": { "bcc": "", "dailyContact": 35, "dailyLimit": 100, "minInterval": 60, "maxInterval": 180, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "eventToStopMails": [] }, "steps": [ { "_id": "6650f1a2b839d80009ac08d2", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800c9e2b951", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [], "options": {} } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d8006d5945c9" ], "processing": true }, "zapierData": [ { "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Update email campaign schedule (global settings) Source: https://docs.emelia.io/docs/campaigns/legacy/email/update-schedule Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `PATCH https://api.emelia.io/emails/campaigns/{campaignId}/settings/global` Replaces the campaign schedule (sending window, pace, tracking). The campaign must not be RUNNING or ARCHIVED. Extra validation beyond the schema: minInterval <= maxInterval, start <= end, dailyLimit >= dailyContact. Changing days/start/end/tracking rewrites each step version's per-day schedule. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `schedule` | object | yes | | | `schedule.bcc` | string (email) | no | Optional BCC address for every email. | | `schedule.dailyContact` | integer | yes | New contacts reached per day. Must be >= 2 to start a campaign. Range: 0 to 500. | | `schedule.dailyLimit` | integer | yes | Max emails sent per day (all steps). Must be >= dailyContact. Range: 1 to 1000. | | `schedule.minInterval` | integer | yes | Min delay between two sends, in SECONDS. Must be <= maxInterval. Range: 1 to 500. | | `schedule.maxInterval` | integer | yes | Max delay between two sends, in SECONDS. Range: 1 to 600. | | `schedule.blacklistUnsub` | boolean | yes | | | `schedule.trackLinks` | boolean | yes | | | `schedule.trackOpens` | boolean | yes | | | `schedule.timeZone` | string | yes | IANA timezone name (moment-timezone), e.g. 'Europe/Paris'. | | `schedule.days` | array of integer | yes | Working days. 0 = Monday ... 6 = Sunday (back convention). | | `schedule.start` | string | yes | Daily window start HH:MM. Must be <= end. | | `schedule.end` | string | yes | | | `schedule.eventToStopMails` | array of enum: `REPLIED`, `CLICKED`, `OPENED` | yes | Events that stop the sequence for a contact. | ```json { "schedule": { "dailyContact": 50, "dailyLimit": 200, "minInterval": 60, "maxInterval": 300, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "eventToStopMails": [ "REPLIED" ] } } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/settings/global" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schedule": { "dailyContact": 50, "dailyLimit": 200, "minInterval": 60, "maxInterval": 300, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "eventToStopMails": [ "REPLIED" ] } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/settings/global', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "schedule": { "dailyContact": 50, "dailyLimit": 200, "minInterval": 60, "maxInterval": 300, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "eventToStopMails": [ "REPLIED" ] } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/settings/global", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "schedule": { "dailyContact": 50, "dailyLimit": 200, "minInterval": 60, "maxInterval": 300, "blacklistUnsub": True, "trackLinks": True, "trackOpens": True, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "eventToStopMails": [ "REPLIED" ] } }, ) print(response.json()) ``` ## Responses ### 200: Updated campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full email campaign document (as returned by create/get/update). | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.provider` | string or null | no | | | `campaign.useManyProviders` | boolean | no | | | `campaign.providersUsed` | array of string | no | | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d800244fe91d", "userId": "6650f1a2b839d800da35bbc5", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "provider": "6650f1a2b839d80045d6927b", "useManyProviders": true, "providersUsed": [ "6650f1a2b839d8002184f429" ], "schedule": { "bcc": "", "dailyContact": 35, "dailyLimit": 100, "minInterval": 60, "maxInterval": 180, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "eventToStopMails": [] }, "steps": [ { "_id": "6650f1a2b839d8001e904670", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d8008207665f", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [], "options": {} } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d8008ddef4f3" ], "processing": true }, "zapierData": [ { "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Update email campaign sending providers Source: https://docs.emelia.io/docs/campaigns/legacy/email/update-mailboxes Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `PATCH https://api.emelia.io/emails/campaigns/{campaignId}/settings/providers` Sets the sending account(s). Either provide provider.id for a single provider, or useManyProviders=true with providersUsed=[...] for rotation. At least one of provider or useManyProviders is required. The campaign must not be RUNNING or ARCHIVED. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `provider` | object | no | Single provider (when useManyProviders is false). | | `provider.id` | string | no | | | `useManyProviders` | boolean | no | | | `providersUsed` | array of string | no | Provider ObjectIds used when useManyProviders is true. | ```json { "useManyProviders": true, "providersUsed": [ "5f3a1b2c4d5e6f0011223344", "5f3a1b2c4d5e6f0011223345" ] } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/settings/providers" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "useManyProviders": true, "providersUsed": [ "5f3a1b2c4d5e6f0011223344", "5f3a1b2c4d5e6f0011223345" ] }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/settings/providers', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "useManyProviders": true, "providersUsed": [ "5f3a1b2c4d5e6f0011223344", "5f3a1b2c4d5e6f0011223345" ] }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/settings/providers", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "useManyProviders": True, "providersUsed": [ "5f3a1b2c4d5e6f0011223344", "5f3a1b2c4d5e6f0011223345" ] }, ) print(response.json()) ``` ## Responses ### 200: Updated campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full email campaign document (as returned by create/get/update). | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.provider` | string or null | no | | | `campaign.useManyProviders` | boolean | no | | | `campaign.providersUsed` | array of string | no | | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d800690d060e", "userId": "6650f1a2b839d800ac974802", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "provider": "6650f1a2b839d80037a61d04", "useManyProviders": true, "providersUsed": [ "6650f1a2b839d8005867cca2" ], "schedule": { "bcc": "", "dailyContact": 35, "dailyLimit": 100, "minInterval": 60, "maxInterval": 180, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "eventToStopMails": [] }, "steps": [ { "_id": "6650f1a2b839d800be3d96c3", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800332199f6", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [], "options": {} } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d80003b0c26e" ], "processing": true }, "zapierData": [ { "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Rename an email campaign Source: https://docs.emelia.io/docs/campaigns/legacy/email/rename-campaign Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `PATCH https://api.emelia.io/emails/campaigns/{campaignId}/name` Renames the campaign. The name must be unique in the workspace. The campaign must not be RUNNING or ARCHIVED. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | | ```json { "name": "Q1 cold outreach (v2)" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/name" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Q1 cold outreach (v2)" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/name', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Q1 cold outreach (v2)" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/name", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Q1 cold outreach (v2)" }, ) print(response.json()) ``` ## Responses ### 200: Updated campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full email campaign document (as returned by create/get/update). | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.provider` | string or null | no | | | `campaign.useManyProviders` | boolean | no | | | `campaign.providersUsed` | array of string | no | | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d800529baa8b", "userId": "6650f1a2b839d800609af433", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "provider": "6650f1a2b839d800d1502d49", "useManyProviders": true, "providersUsed": [ "6650f1a2b839d80034816367" ], "schedule": { "bcc": "", "dailyContact": 35, "dailyLimit": 100, "minInterval": 60, "maxInterval": 180, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "eventToStopMails": [] }, "steps": [ { "_id": "6650f1a2b839d8005885e18a", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800746ebf19", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [], "options": {} } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d800990ca821" ], "processing": true }, "zapierData": [ { "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Replace email campaign steps Source: https://docs.emelia.io/docs/campaigns/legacy/email/replace-steps Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `PATCH https://api.emelia.io/emails/campaigns/{campaignId}/steps` Replaces the full ordered list of steps (the sequence). The campaign must not be RUNNING or ARCHIVED. Steps after the first must have delay.amount > 0, and a step may have at most one disabled version. Changing the step count resets the cached stats. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `steps` | array of object | yes | | | `steps[].delay` | object | yes | | | `steps[].delay.amount` | integer | no | Must be > 0 for steps after the first. Range: 0 to 10000. | | `steps[].delay.unit` | enum: `MINUTES`, `HOURS`, `DAYS` | yes | | | `steps[].versions` | array of object | yes | A/B versions. At most one may be disabled. | | `steps[].versions[].subject` | string | no | | | `steps[].versions[].disabled` | boolean | no | | | `steps[].versions[].message` | string | no | HTML body. Variables use {{firstName}}, {{custom_field}}, {{UNSUBSCRIBE_LINK}}, {{SENDER}}, {{UNIQUE_ID}}. | | `steps[].versions[].rawHtml` | boolean | no | | | `steps[].versions[].attachments` | array of object | no | | | `steps[].versions[].options` | object | no | Per-version overrides (per-day schedule, tracking, provider). Usually omitted; derived from the campaign schedule. | ```json { "steps": [ { "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

" } ] }, { "delay": { "amount": 2, "unit": "DAYS" }, "versions": [ { "subject": "", "message": "

Following up.

" } ] } ] } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/steps" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

" } ] }, { "delay": { "amount": 2, "unit": "DAYS" }, "versions": [ { "subject": "", "message": "

Following up.

" } ] } ] }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/steps', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "steps": [ { "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

" } ] }, { "delay": { "amount": 2, "unit": "DAYS" }, "versions": [ { "subject": "", "message": "

Following up.

" } ] } ] }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/steps", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "steps": [ { "delay": { "amount": 0, "unit": "DAYS" }, "versions": [ { "subject": "Quick question, {{firstName}}", "message": "

Hi {{firstName}},

" } ] }, { "delay": { "amount": 2, "unit": "DAYS" }, "versions": [ { "subject": "", "message": "

Following up.

" } ] } ] }, ) print(response.json()) ``` ## Responses ### 200: Updated campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full email campaign document (as returned by create/get/update). | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.provider` | string or null | no | | | `campaign.useManyProviders` | boolean | no | | | `campaign.providersUsed` | array of string | no | | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.zapierData` | array of object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d8000b13890b", "userId": "6650f1a2b839d800555fdeb3", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "provider": "6650f1a2b839d800d6193fc9", "useManyProviders": true, "providersUsed": [ "6650f1a2b839d800c15749e7" ], "schedule": { "bcc": "", "dailyContact": 35, "dailyLimit": 100, "minInterval": 60, "maxInterval": 180, "blacklistUnsub": true, "trackLinks": true, "trackOpens": true, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "eventToStopMails": [] }, "steps": [ { "_id": "6650f1a2b839d8004d4acc0a", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800ef395699", "subject": "Quick question about Nexora", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "rawHtml": true, "attachments": [], "options": {} } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d800cdc683a1" ], "processing": true }, "zapierData": [ { "webhookUrl": "https://hooks.yourcompany.com/emelia", "events": [] } ], "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Start an email campaign Source: https://docs.emelia.io/docs/campaigns/legacy/email/start-campaign Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `POST https://api.emelia.io/emails/campaigns/{campaignId}/start` Starts the campaign (status RUNNING). Requires an active subscription and passes start checks: at least one provider connected, a schedule, at least one step, at least one recipient, dailyContact >= 2, and at most 30000 recipients (higher for beta accounts). Validation failures return a stable error code (e.g. EMAIL_PROVIDER_MISSING, NO_RECIPIENTS, TOO_MANY_RECIPIENTS, DAILY_CONTACT_TOO_LOW). New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/start" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/start', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/start", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Campaign started. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Start refused (missing provider/steps/recipients, quota, etc.). Body carries a stable code and params. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Pause an email campaign Source: https://docs.emelia.io/docs/campaigns/legacy/email/pause-campaign Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `POST https://api.emelia.io/emails/campaigns/{campaignId}/pause` Pauses a RUNNING campaign (status PAUSED) and clears its scheduled jobs. Errors if the campaign is not RUNNING. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/pause" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/pause', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/pause", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Campaign paused. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Set a custom field on an email contact Source: https://docs.emelia.io/docs/campaigns/legacy/email/set-contact-field Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `PATCH https://api.emelia.io/emails/contacts` Sets or updates one field on the contact identified by email inside the campaign (campaignId). Known keys (firstName, lastName, phoneNumber...) update the field; any other name is stored under custom. Spaces in fieldName are removed. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | | | `email` | string (email) | yes | | | `fieldName` | string | yes | | | `fieldValue` | string | yes | | ```json { "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com", "fieldName": "company", "fieldValue": "Acme Inc" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/emails/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com", "fieldName": "company", "fieldValue": "Acme Inc" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/contacts', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com", "fieldName": "company", "fieldValue": "Acme Inc" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/emails/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "john.doe@acme.com", "fieldName": "company", "fieldValue": "Acme Inc" }, ) print(response.json()) ``` ## Responses ### 200: Field set. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Get email campaign activities Source: https://docs.emelia.io/docs/campaigns/legacy/email/get-activities Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `GET https://api.emelia.io/emails/campaigns/{campaignId}/activities` Returns a page of activity events for the campaign (30 per page, page is 0-based). RE_REPLY and (internally) OPENED are excluded. Filter with type (single event) or types (comma-separated list), by contactId or versionId, and search (tokens on firstName/lastName/email) or query (exact email). New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `contactId` | string | no | | | `versionId` | string | no | | | `type` | enum: `SENT`, `FIRST_OPEN`, `OPENED`, `CLICKED`, `REPLIED`, `BOUNCED`, `UNSUBSCRIBED` | no | | | `types` | string | no | Comma-separated list of events. | | `page` | string | no | 0-based page index. 30 items per page. | | `query` | string | no | Exact email match. | | `search` | string | no | Token search on firstName/lastName/email. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/activities" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/activities', { 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/emails/campaigns/65f1a2b3c4d5e6f73478765e/activities", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Activities (note: envelope is { activities }, no success flag). | Field | Type | Required | Description | |---|---|---|---| | `activities` | array of object | yes | | | `activities[]._id` | string | no | | | `activities[].contact` | string or object | no | Contact id, or the populated contact. | | `activities[].event` | enum: `SENT`, `FIRST_OPEN`, `OPENED`, `CLICKED`, `REPLIED`, `BOUNCED`, `UNSUBSCRIBED` | no | | | `activities[].date` | string (date-time) | no | | | `activities[].step` | integer | no | 0-based step index, -1 if unresolved. | | `activities[].version` | integer | no | 0-based version index, -1 when the step has a single version. | | `activities[].customData` | object | no | Event metadata (internal keys are stripped). | ```json { "activities": [ { "_id": "6650f1a2b839d800bac4cdd2", "contact": "6650f1a2b839d8003c2df81e", "event": "SENT", "date": "2026-10-06T09:30:00.000Z", "step": 0, "version": 0, "customData": {} } ] } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Send a test email Source: https://docs.emelia.io/docs/campaigns/legacy/email/send-test-email Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `POST https://api.emelia.io/emails/test` Sends a test of one step/version to an address. Target the step/version either by 0-based indices (step, version) or by stepId/versionId. At least one of step, stepId or versionId is required; version defaults to 0. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | | | `email` | string (email) | yes | | | `step` | integer | no | 0-based step index. Range: 0 to .... | | `version` | integer | no | 0-based A/B version index. Default: `0`. Range: 0 to .... | | `stepId` | string | no | | | `versionId` | string | no | | ```json { "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "qa@acme.com", "step": 0, "version": 0 } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/test" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "qa@acme.com", "step": 0, "version": 0 }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/test', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "qa@acme.com", "step": 0, "version": 0 }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/test", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "campaignId": "5f3a1b2c4d5e6f0011223344", "email": "qa@acme.com", "step": 0, "version": 0 }, ) print(response.json()) ``` ## Responses ### 200: Test sent. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Get email campaign statistics Source: https://docs.emelia.io/docs/campaigns/legacy/email/get-statistics Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md) `GET https://api.emelia.io/stats` Returns aggregated statistics for one email campaign. By default (detailed omitted or not 'true') a flat summary is returned; with detailed=true a { global, steps } breakdown is returned. Statistics are computed on demand and cached. PITFALLS: when the campaign is not found the route answers 401 (not 404) with { error: 'Campaign not found' }; ANY internal error also answers 401 { error: 'You must be logged in' }. detailed is read as the string 'true'; any other value (including the boolean true serialised elsewhere) is treated as the summary. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | | | `detailed` | boolean | no | Pass true for the per-step breakdown. | | `start` | string | no | ISO start date filter. | | `end` | string | no | | | `providerId` | string | no | Restrict stats to one provider. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/stats?campaignId=65f1a2b3c4d5e6f73478765e" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/stats?campaignId=65f1a2b3c4d5e6f73478765e', { 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/stats?campaignId=65f1a2b3c4d5e6f73478765e", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Summary (default) or detailed stats. ```json { "mailsSent": 1200, "uniqueOpensPercent": 48.5, "opens": 1700, "linkClickedPercent": 12.3, "repliedPercent": 6.1, "bouncedPercent": 1.2, "unsubscribePercent": 0.4, "progressPercent": 73 } ``` ### 401: Campaign not found, or any internal/auth error (this route returns 401 for both). | 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" } ``` ### 503: Statistics are still being computed (only for accounts under single-flight), retry after the Retry-After delay. | 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": "Statistics are being computed, please try again in a few moments", "code": "STATS_COMPUTING" } ``` --- # LinkedIn campaigns > The routes of the previous LinkedIn only campaigns. Deprecated, still working for existing integrations. Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin > **Deprecated.** These routes manage the LinkedIn only campaigns of the previous system. They keep working, but new integrations should create [Campaigns](https://docs.emelia.io/docs/campaigns.md) with LinkedIn steps (`VISIT`, `CONNECTION`, `MESSAGE`...), see [A LinkedIn automation sequence](https://docs.emelia.io/docs/campaigns.md#a-linkedin-automation-sequence). ## What you can still do | Need | Route | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | List the legacy LinkedIn campaigns | [List campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin/list-campaigns.md) | | Feed a running legacy campaign | [Add a contact](https://docs.emelia.io/docs/campaigns/legacy/linkedin/add-contact.md) | | Read the results | [Get activities](https://docs.emelia.io/docs/campaigns/legacy/linkedin/get-activities.md), [Get statistics](https://docs.emelia.io/docs/campaigns/legacy/linkedin/get-statistics.md) | [`POST /linkedin/campaign/contacts`](https://docs.emelia.io/docs/campaigns/legacy/linkedin/add-contact.md) **curl** ```bash curl -X POST https://api.emelia.io/linkedin/campaign/contacts \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id": "6650f1a2b839d80011c5a0f1", "contact": {"linkedinUrlProfile": "https://www.linkedin.com/in/claire-fontaine", "firstName": "Claire", "lastName": "Fontaine"}}' ``` **JavaScript** ```js await fetch('https://api.emelia.io/linkedin/campaign/contacts', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ id: '6650f1a2b839d80011c5a0f1', contact: { linkedinUrlProfile: 'https://www.linkedin.com/in/claire-fontaine', firstName: 'Claire', lastName: 'Fontaine' }, }), }); ``` **Python** ```python import os import requests requests.post( "https://api.emelia.io/linkedin/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "6650f1a2b839d80011c5a0f1", "contact": {"linkedinUrlProfile": "https://www.linkedin.com/in/claire-fontaine", "firstName": "Claire", "lastName": "Fontaine"}, }, ) ``` The profile goes to a list named after the campaign. A finished campaign starts again, a running one schedules the contact at once, and a profile already in that list is refused. ## Good to know * The listing of contacts pages from `page=0` and answers `{ success, list, count }`. * `PATCH .../settings/global` updates the fields you send, with `dailyContact` from 1 to 80. * `GET /stats/linkedin` answers `401` for an unknown campaign, and a flat summary unless `detailed=true`. * The LinkedIn events of these campaigns (`VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `REPLIED`) are described in [Legacy LinkedIn activity](https://docs.emelia.io/docs/webhooks/legacy-linkedin-activity.md). --- # List LinkedIn campaign contacts (paginated) Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/list-campaign-contacts Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `GET https://api.emelia.io/linkedin/campaign/contacts` Returns a page of the campaign recipients. Pagination is 0-based; perPage defaults to 20 (min 10, max 10000). The response envelope is { success, list, count } (NOT { contacts, total }). event filters by status and accepts either an array or a single string (unlike the email variant which requires an array). New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | | | `page` | integer | yes | 0-based page index. | | `perPage` | integer | no | Default 20, max 10000. | | `event` | array of enum: `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `REPLIED`, `NO_ACTIVITY` | no | Status filter; a single string is also accepted. | | `query` | string | no | Token search on handle/firstName/lastName. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/linkedin/campaign/contacts?id=65f1a2b3c4d5e6f73478765e&page=0" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaign/contacts?id=65f1a2b3c4d5e6f73478765e&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/linkedin/campaign/contacts?id=65f1a2b3c4d5e6f73478765e&page=0", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: One page of contacts. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `list` | array of object | yes | | | `list[]._id` | string | no | | | `list[].url` | string | no | | | `list[].handle` | string | no | | | `list[].firstName` | string | no | | | `list[].lastName` | string | no | | | `list[].custom` | object | no | | | `list[].status` | string | no | e.g. VISITED, INVITED, ACCEPTED, MESSAGE_SENT, INMAIL_SENT, REPLIED, EXCLUDED, CANT_CONNECT. '-' when none. | | `list[].lastContacted` | string (date-time) | no | | | `list[].lastReplied` | string (date-time) | no | | | `count` | integer | yes | | ```json { "success": true, "list": [ { "_id": "6650f1a2b839d800be1b6ee1", "url": "https://www.linkedin.com/in/claire-fontaine", "handle": "claire-fontaine", "firstName": "Claire", "lastName": "Fontaine", "custom": {}, "status": "INVITED", "lastContacted": "2026-10-06T09:30:00.000Z", "lastReplied": "2026-10-06T09:30:00.000Z" } ], "count": 250 } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Add a contact to a LinkedIn campaign Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/add-contact Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `POST https://api.emelia.io/linkedin/campaign/contacts` Adds one profile to the LinkedIn campaign (id). The profile is appended to a dedicated list ' contacts from API'. Side effects: a FINISHED campaign is restarted; a RUNNING campaign schedules the new contact. Fails if the profile is already in that list. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | LinkedIn campaign ObjectId. | | `contact` | object | yes | | | `contact.linkedinUrlProfile` | string | yes | Profile URL (linkedin.com/in/..., or a Sales Navigator lead/people URL). | | `contact.firstName` | string | no | | | `contact.lastName` | string | no | | ```json { "id": "5f3a1b2c4d5e6f0011223390", "contact": { "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe/", "firstName": "John", "lastName": "Doe" } } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin/campaign/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "5f3a1b2c4d5e6f0011223390", "contact": { "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe/", "firstName": "John", "lastName": "Doe" } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaign/contacts', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": "5f3a1b2c4d5e6f0011223390", "contact": { "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe/", "firstName": "John", "lastName": "Doe" } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "5f3a1b2c4d5e6f0011223390", "contact": { "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe/", "firstName": "John", "lastName": "Doe" } }, ) print(response.json()) ``` ## Responses ### 200: Contact added. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Remove a contact from a LinkedIn campaign Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/remove-contact Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `DELETE https://api.emelia.io/linkedin/campaign/contacts` Removes the contact whose profile URL matches from the campaign (id). Use id='ALL_CAMPAIGNS' to remove from every non-archived LinkedIn campaign. Body field is 'url'. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Campaign ObjectId or 'ALL_CAMPAIGNS'. | | `url` | string | yes | LinkedIn profile URL. | ```json { "id": "5f3a1b2c4d5e6f0011223390", "url": "https://www.linkedin.com/in/john-doe/" } ``` ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/linkedin/campaign/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "5f3a1b2c4d5e6f0011223390", "url": "https://www.linkedin.com/in/john-doe/" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaign/contacts', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "id": "5f3a1b2c4d5e6f0011223390", "url": "https://www.linkedin.com/in/john-doe/" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/linkedin/campaign/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "id": "5f3a1b2c4d5e6f0011223390", "url": "https://www.linkedin.com/in/john-doe/" }, ) print(response.json()) ``` ## Responses ### 200: Removed (always success:true). | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # List LinkedIn campaigns Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/list-campaigns Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `GET https://api.emelia.io/linkedin/campaigns` Returns non-archived LinkedIn campaigns with a reduced field set (_id, name, status, createdAt, startAt, plannedStart, account) and the account populated to { _id, metadata }. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/linkedin/campaigns" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaigns', { 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/linkedin/campaigns", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Campaigns. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaigns` | array of object | yes | | | `campaigns[]._id` | string | no | | | `campaigns[].name` | string | no | | | `campaigns[].status` | string | no | | | `campaigns[].createdAt` | string (date-time) | no | | | `campaigns[].startAt` | string (date-time) | no | | | `campaigns[].plannedStart` | string (date-time) | no | | | `campaigns[].account` | object or null | no | Populated to { _id, metadata }. | ```json { "success": true, "campaigns": [ { "_id": "6650f1a2b839d800cc3c9381", "name": "Q4 outreach: SaaS founders", "status": "RUNNING", "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z", "account": {} } ] } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Create a LinkedIn campaign Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/create-campaign Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `POST https://api.emelia.io/linkedin/campaigns` Creates an empty LinkedIn campaign in DRAFT with one VISIT step. name must be unique. Returns the full created campaign. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | | ```json { "name": "LinkedIn Q1 outreach" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin/campaigns" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "LinkedIn Q1 outreach" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaigns', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "LinkedIn Q1 outreach" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin/campaigns", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "LinkedIn Q1 outreach" }, ) print(response.json()) ``` ## Responses ### 200: Campaign created. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full LinkedIn campaign document. | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.account` | string or null | no | LinkedIn account (authe) id. | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d80006f33988", "userId": "6650f1a2b839d80009687764", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "account": "6650f1a2b839d80057878a84", "schedule": { "dailyContact": 35, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "excludeAlreadyMessaged": true, "exclude1stConn": true, "excludeNoPictures": true }, "steps": [ { "_id": "6650f1a2b839d8007455fc19", "stepType": "VISIT", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800ccca7024", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "attachments": [] } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d80009bc84a0" ], "processing": true }, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Update LinkedIn campaign schedule (global settings) Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/update-schedule Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `PATCH https://api.emelia.io/linkedin/campaigns/{campaignId}/settings/global` Updates the LinkedIn schedule. The campaign must not be RUNNING or ARCHIVED. Fields are written one by one: omitting a field leaves it unchanged (so the V3 front can skip exclude1stConn / excludeNoPictures). start <= end and 1 <= dailyContact <= 80. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `schedule` | object | yes | | | `schedule.dailyContact` | integer | yes | Prospects reached per day. Enforced 1..80 here; the start check caps it at 40 when a CONNECTION step exists, else 80. | | `schedule.timeZone` | string | yes | IANA timezone name. | | `schedule.days` | array of integer | yes | 0 = Monday ... 6 = Sunday. | | `schedule.start` | string | yes | | | `schedule.end` | string | yes | | | `schedule.excludeAlreadyMessaged` | boolean | no | | | `schedule.exclude1stConn` | boolean | no | | | `schedule.excludeNoPictures` | boolean | no | | ```json { "schedule": { "dailyContact": 25, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "excludeAlreadyMessaged": true } } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/settings/global" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "schedule": { "dailyContact": 25, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "excludeAlreadyMessaged": true } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/settings/global', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "schedule": { "dailyContact": 25, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "excludeAlreadyMessaged": true } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/settings/global", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "schedule": { "dailyContact": 25, "timeZone": "Europe/Paris", "days": [ 0, 1, 2, 3, 4 ], "start": "09:00", "end": "18:00", "excludeAlreadyMessaged": True } }, ) print(response.json()) ``` ## Responses ### 200: Updated campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full LinkedIn campaign document. | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.account` | string or null | no | LinkedIn account (authe) id. | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d800b55a762a", "userId": "6650f1a2b839d800cd3b1c2e", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "account": "6650f1a2b839d800e4ec0ae6", "schedule": { "dailyContact": 35, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "excludeAlreadyMessaged": true, "exclude1stConn": true, "excludeNoPictures": true }, "steps": [ { "_id": "6650f1a2b839d800a5cf17c7", "stepType": "VISIT", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d80055fb9fba", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "attachments": [] } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d800b5bce4fa" ], "processing": true }, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Rename a LinkedIn campaign Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/rename-campaign Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `PATCH https://api.emelia.io/linkedin/campaigns/{campaignId}/name` Renames the campaign (name must be unique). The campaign must not be RUNNING or ARCHIVED. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | | ```json { "name": "LinkedIn Q1 (v2)" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/name" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "LinkedIn Q1 (v2)" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/name', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "LinkedIn Q1 (v2)" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/name", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "LinkedIn Q1 (v2)" }, ) print(response.json()) ``` ## Responses ### 200: Updated campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full LinkedIn campaign document. | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.account` | string or null | no | LinkedIn account (authe) id. | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d800945fe8f2", "userId": "6650f1a2b839d8008cb52536", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "account": "6650f1a2b839d8002aad97ee", "schedule": { "dailyContact": 35, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "excludeAlreadyMessaged": true, "exclude1stConn": true, "excludeNoPictures": true }, "steps": [ { "_id": "6650f1a2b839d80094bcf85f", "stepType": "VISIT", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800e3549022", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "attachments": [] } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d80070a03472" ], "processing": true }, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Replace LinkedIn campaign steps Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/replace-steps Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `PATCH https://api.emelia.io/linkedin/campaigns/{campaignId}/steps` Replaces the full ordered list of LinkedIn steps. The campaign must not be RUNNING or ARCHIVED. Steps after the first need delay.amount > 0; at most one disabled version per step. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `steps` | array of object | yes | | | `steps[].stepType` | enum: `VISIT`, `CONNECTION`, `MESSAGE`, `LIKE`, `FOLLOW`, `INMAIL` | yes | `FOLLOW` is accepted when saved, but nothing runs it: do not use it. | | `steps[].delay` | object | yes | | | `steps[].delay.amount` | integer | no | Must be > 0 for steps after the first. Range: 0 to 10000. | | `steps[].delay.unit` | enum: `MINUTES`, `HOURS`, `DAYS` | yes | | | `steps[].versions` | array of object | yes | At most one disabled version per step. VISIT/LIKE/FOLLOW steps can carry an empty versions array. | | `steps[].versions[].disabled` | boolean | no | | | `steps[].versions[].message` | string | no | | | `steps[].versions[].attachments` | array of object | no | | ```json { "steps": [ { "stepType": "VISIT", "delay": { "amount": 0, "unit": "DAYS" }, "versions": [] }, { "stepType": "CONNECTION", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, happy to connect." } ] } ] } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/steps" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "steps": [ { "stepType": "VISIT", "delay": { "amount": 0, "unit": "DAYS" }, "versions": [] }, { "stepType": "CONNECTION", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, happy to connect." } ] } ] }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/steps', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "steps": [ { "stepType": "VISIT", "delay": { "amount": 0, "unit": "DAYS" }, "versions": [] }, { "stepType": "CONNECTION", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, happy to connect." } ] } ] }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/steps", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "steps": [ { "stepType": "VISIT", "delay": { "amount": 0, "unit": "DAYS" }, "versions": [] }, { "stepType": "CONNECTION", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "message": "Hi {{firstName}}, happy to connect." } ] } ] }, ) print(response.json()) ``` ## Responses ### 200: Updated campaign. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | | `campaign` | object | yes | A full LinkedIn campaign document. | | `campaign._id` | string | no | | | `campaign.userId` | string | no | | | `campaign.name` | string | no | | | `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no | | | `campaign.account` | string or null | no | LinkedIn account (authe) id. | | `campaign.schedule` | object | no | | | `campaign.steps` | array of object | no | | | `campaign.recipients` | object | no | | | `campaign.createdAt` | string (date-time) | no | | | `campaign.startAt` | string (date-time) | no | | | `campaign.estimatedEnd` | string (date-time) | no | | | `campaign.plannedStart` | string (date-time) | no | | ```json { "success": true, "campaign": { "_id": "6650f1a2b839d800941eec2c", "userId": "6650f1a2b839d800d0a32198", "name": "Q4 outreach: SaaS founders", "status": "DRAFT", "account": "6650f1a2b839d800463fada8", "schedule": { "dailyContact": 35, "timeZone": "Europe/Paris", "days": [ 1 ], "start": "08:00", "end": "17:00", "excludeAlreadyMessaged": true, "exclude1stConn": true, "excludeNoPictures": true }, "steps": [ { "_id": "6650f1a2b839d80036a70635", "stepType": "VISIT", "delay": { "amount": 1, "unit": "DAYS" }, "versions": [ { "_id": "6650f1a2b839d800f3a30d30", "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?", "disabled": false, "attachments": [] } ] } ], "recipients": { "lists": [ {} ], "contacts": [ "6650f1a2b839d800fd48d7a4" ], "processing": true }, "createdAt": "2026-10-06T09:30:00.000Z", "startAt": "2026-10-06T09:30:00.000Z", "estimatedEnd": "2026-10-06T09:30:00.000Z", "plannedStart": "2026-10-06T09:30:00.000Z" } } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Get LinkedIn campaign activities Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/get-activities Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `GET https://api.emelia.io/linkedin/campaigns/{campaignId}/activities` Returns a page of LinkedIn activity events (30 per page, 0-based). RE_REPLY and FAILED_ACTION are excluded; type=MESSAGE_SENT also returns INMAIL_SENT. Filter by contactId, versionId, type or types (comma list), and search (tokens on firstName/lastName/url). New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Mongo ObjectId of the campaign. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `contactId` | string | no | | | `versionId` | string | no | | | `type` | enum: `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `REPLIED`, `FOLLOWED`, `LIKED` | no | | | `types` | string | no | | | `page` | string | no | 0-based, 30 per page. | | `query` | string | no | | | `search` | string | no | | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/activities" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/activities', { 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/linkedin/campaigns/65f1a2b3c4d5e6f73478765e/activities", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Activities (envelope is { activities }, no success flag). | Field | Type | Required | Description | |---|---|---|---| | `activities` | array of object | yes | | | `activities[]._id` | string | no | | | `activities[].contact` | string or object | no | | | `activities[].event` | enum: `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `REPLIED`, `FOLLOWED`, `LIKED` | no | | | `activities[].date` | string (date-time) | no | | | `activities[].step` | integer | no | | | `activities[].version` | integer | no | | | `activities[].customData` | object | no | | ```json { "activities": [ { "_id": "6650f1a2b839d8003f9f699f", "contact": "6650f1a2b839d8005b0244b3", "event": "VISITED", "date": "2026-10-06T09:30:00.000Z", "step": 0, "version": 0, "customData": {} } ] } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Get LinkedIn campaign statistics Source: https://docs.emelia.io/docs/campaigns/legacy/linkedin/get-statistics Guide: [LinkedIn campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin.md) `GET https://api.emelia.io/stats/linkedin` Returns aggregated statistics for one LinkedIn campaign. Summary by default; detailed=true gives { global, steps }. Computed on demand and cached. PITFALLS: a missing campaign answers 401 { error: 'Campaign not found' }, and any internal error answers 401 { error: 'You must be logged in' }. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | | | `detailed` | boolean | no | | | `start` | string | no | | | `end` | string | no | | | `accountId` | string | no | Restrict stats to one LinkedIn account. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/stats/linkedin?campaignId=65f1a2b3c4d5e6f73478765e" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/stats/linkedin?campaignId=65f1a2b3c4d5e6f73478765e', { 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/stats/linkedin?campaignId=65f1a2b3c4d5e6f73478765e", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Summary (default) or detailed stats. ```json { "visited": 300, "invited": 250, "accepted": 120, "message_sent": 120, "replied": 35, "progress_percent": 60, "invite_percent": 48 } ``` ### 401: Campaign not found, or any internal/auth error. | 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" } ``` --- # Lists & contacts > Create contact lists, add and update contacts one by one or 100 at a time, declare custom variables, look a contact up, export lists, and feed running campaigns. Source: https://docs.emelia.io/docs/lists ## The model Contacts live in **lists**. A campaign reads one or several lists, its recipients. * Add a contact to a list used by a campaign: the contact enters the campaign, usually within seconds. * Update a contact: the new values reach the campaigns that use its list. * Delete a contact from the list: it leaves those campaigns. This is how you keep a campaign alive: you add to the list, never to the campaign. A list attached to two campaigns feeds both, so a contact would receive both sequences. Lists, contacts and exports never cost credits. > **A separate service.** The lists and contacts routes are served under `https://api.emelia.io/lists`, with the same key. They are not counted in the per minute quota, their errors carry a `message` field instead of `error`, and they use real `404`, `413` and `422` statuses. See [Errors](https://docs.emelia.io/docs/errors.md). The Python examples of this guide share one session: ```python title="Python setup" import os import requests session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] list_id = "66f1c0ffee5a7d3b2c4e8a10" ``` ## Create a list [`POST /lists/list`](https://docs.emelia.io/docs/lists/create-list.md) **curl** ```bash curl -X POST https://api.emelia.io/lists/list \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Q4 SaaS founders"}' ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/lists/list', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Q4 SaaS founders' }), }); const { listId } = await res.json(); // 201 ``` **Python** ```python res = session.post("https://api.emelia.io/lists/list", json={"name": "Q4 SaaS founders"}) list_id = res.json()["listId"] # 201 ``` Give each list the name of the campaign it feeds: it saves time later. ## Declare your custom variables first Any key that is not a standard field becomes a custom variable, shared by all your lists. Declare yours before the first import: an existing variable is matched before Emelia guesses from the value, which avoids most mapping surprises. [`POST /lists/custom-variable`](https://docs.emelia.io/docs/lists/create-custom-variable.md) **curl** ```bash curl -X POST https://api.emelia.io/lists/custom-variable \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"technicalName": "icebreaker", "schema": "lead", "displayName": "Icebreaker"}' ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/lists/custom-variable', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ technicalName: 'icebreaker', schema: 'lead', displayName: 'Icebreaker' }), }); // 422 means the name already exists: safe to ignore. ``` **Python** ```python res = session.post( "https://api.emelia.io/lists/custom-variable", json={"technicalName": "icebreaker", "schema": "lead", "displayName": "Icebreaker"}, ) # 422 means the name already exists: safe to ignore. ``` * `schema` is `lead` for a contact variable, `company` for a company variable. This route is the only way to create a company variable. * The call answers `422` when the name exists or is a standard field: call it at every import and treat `422` as "already there". [Check a custom variable](https://docs.emelia.io/docs/lists/check-custom-variable.md) tells you in advance. * Variables cannot be renamed or deleted through the API. ## Add one contact [`POST /lists/list/{listId}/contacts`](https://docs.emelia.io/docs/lists/add-contact.md) The body is a **flat object**: one key per field, values that are strings, numbers or booleans. **curl** ```bash curl -X POST "https://api.emelia.io/lists/list/66f1c0ffee5a7d3b2c4e8a10/contacts?updateIfExists=true" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Claire", "lastName": "Fontaine", "email": "claire.fontaine@nexora.io", "jobTitle": "CTO", "linkedinUrlProfile": "https://www.linkedin.com/in/claire-fontaine", "companyName": "Nexora", "websiteUrl": "https://nexora.io", "icebreaker": "your move to Postgres 16 last month" }' ``` **JavaScript** ```js const listId = '66f1c0ffee5a7d3b2c4e8a10'; const res = await fetch(`https://api.emelia.io/lists/list/${listId}/contacts?updateIfExists=true`, { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ firstName: 'Claire', lastName: 'Fontaine', email: 'claire.fontaine@nexora.io', jobTitle: 'CTO', linkedinUrlProfile: 'https://www.linkedin.com/in/claire-fontaine', companyName: 'Nexora', websiteUrl: 'https://nexora.io', icebreaker: 'your move to Postgres 16 last month', }), }); const { leadId, duplicate, updated } = await res.json(); ``` **Python** ```python res = session.post( f"https://api.emelia.io/lists/list/{list_id}/contacts", params={"updateIfExists": "true"}, json={ "firstName": "Claire", "lastName": "Fontaine", "email": "claire.fontaine@nexora.io", "jobTitle": "CTO", "linkedinUrlProfile": "https://www.linkedin.com/in/claire-fontaine", "companyName": "Nexora", "websiteUrl": "https://nexora.io", "icebreaker": "your move to Postgres 16 last month", }, ) print(res.json()) ``` ### Duplicates and updates Before creating, Emelia looks in this list, and in the lists linked to it, for a contact with the same `email`, then with the same `linkedinUrlProfile`. If one is found, nothing is created: the response returns its `leadId` with `duplicate: true`. With `?updateIfExists=true` (the exact string `true`, as a query parameter), a duplicate is **updated** instead: the fields you send overwrite the stored values, the others stay, and the response says `updated: true`. | Situation | `duplicate` | `updated` | | -------------------------------------------- | ----------- | --------- | | New contact | `false` | `false` | | Already in the list, `updateIfExists` absent | `true` | `false` | | Already in the list, `updateIfExists=true` | `true` | `true` | The status is `200` in all three cases. Three things to know: * **The email match is exact and case sensitive.** `Claire@Nexora.io` and `claire@nexora.io` make two contacts. Send emails in lowercase. * **The LinkedIn URL is compared as a string.** A trailing slash or a missing `www.` makes another URL. Send one canonical form, like `https://www.linkedin.com/in/claire-fontaine`. * **A contact without email and without LinkedIn URL is never deduplicated:** every retry creates a new one. ## Add up to 100 contacts [`POST /lists/list/{listId}/contacts/batch`](https://docs.emelia.io/docs/lists/add-contacts-in-bulk.md) Same objects, wrapped in `contacts`. Each row is processed on its own: a failing row is reported and does not stop the others. **curl** ```bash curl -X POST "https://api.emelia.io/lists/list/66f1c0ffee5a7d3b2c4e8a10/contacts/batch?updateIfExists=true" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "firstName": "Claire", "email": "claire.fontaine@nexora.io", "companyName": "Nexora" }, { "firstName": "Hugo", "email": "hugo.martin@brightlane.io", "companyName": "Brightlane" } ] }' ``` **JavaScript** ```js // Load a large array 100 rows at a time. async function loadContacts(listId, rows) { const report = { created: 0, duplicates: 0, updated: 0, failed: [] }; for (let i = 0; i < rows.length; i += 100) { const chunk = rows.slice(i, i + 100); const res = await fetch(`https://api.emelia.io/lists/list/${listId}/contacts/batch?updateIfExists=true`, { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ contacts: chunk }), }); const body = await res.json(); report.created += body.created; report.duplicates += body.duplicates; report.updated += body.updated; for (const r of body.results) if (r.error) report.failed.push({ row: chunk[r.index], error: r.error }); } return report; } ``` **Python** ```python def load_contacts(list_id, rows): report = {"created": 0, "duplicates": 0, "updated": 0, "failed": []} for i in range(0, len(rows), 100): chunk = rows[i : i + 100] body = session.post( f"https://api.emelia.io/lists/list/{list_id}/contacts/batch", params={"updateIfExists": "true"}, json={"contacts": chunk}, ).json() for key in ("created", "duplicates", "updated"): report[key] += body[key] report["failed"] += [ {"row": chunk[r["index"]], "error": r["error"]} for r in body["results"] if "error" in r ] return report ``` ```json title="Response" { "success": true, "total": 3, "created": 1, "duplicates": 1, "updated": 0, "failed": 1, "results": [ { "index": 0, "leadId": "66f2a1b2c3d4e5f6a7b8c9d0", "companyId": "66f2a1b2c3d4e5f6a7b8c9e0", "duplicate": true, "updated": false }, { "index": 1, "leadId": "66f2a1b2c3d4e5f6a7b8c9d1", "companyId": "66f2a1b2c3d4e5f6a7b8c9e1", "duplicate": false, "updated": false }, { "index": 2, "error": "Field \"tags\" must be a string, number or boolean" } ], "createdCustomVariables": [] } ``` `results` follows the order of your array: `index` points back to the row you sent. Counters add up: `created + duplicates + failed = total`, and `updated` is part of `duplicates`. A row that repeats an email already seen earlier in the same batch is a duplicate of that row and is never updated. Limits: 1 to 100 contacts per call (`422` otherwise), and 5 MB per body (`413`, `PAYLOAD_TOO_LARGE`). ## How keys are read Each key of a contact is resolved in this order: 1. **The name of a standard field or one of its aliases**, whatever the case: `firstName`, `First Name` or `Prénom` all land in `firstName`. 2. **The technical name of a standard field**, ignoring case, spaces, `_` and `-` (`first_name` works), and also as a prefix or a suffix when the key has 3 characters or more. 3. **One of your custom variables**, by technical or display name. 4. A key that ends in `id` becomes a custom variable, without looking at its value. 5. **The value**: an email goes to `email`, 6 digits or more go to `phone`, a `linkedin.com/in/` URL goes to `linkedinUrlProfile`, another `http(s)://` URL goes to the company `websiteUrl`. 6. Otherwise, **a new custom variable** is created and returned in `createdCustomVariables`. Traps that come from this order: * **Short keys are captured by prefix.** `job` lands in `jobStartDate` (a date: the contact fails), `last` in `lastContacted`, `year` in `yearsOfExperience`, `note` in the company `notes`. Use the exact keys below. * **Numbers sent under an unknown key go to `phone`**: a company registration number, an order number, a date such as `2024-01-15`. Declare the custom variable first: step 3 comes before step 5. * **`mobile` goes to `phone`.** Use `mobilePhone` for a mobile number. * **A standard field takes one key per contact.** A second email key moves to the next matching field and can be lost. * **`null` and empty strings are ignored**, so a field cannot be cleared by the API. Arrays and objects make the row fail. ### Standard fields | Contact | Company | | ---------------------------------------------------------------------- | ----------------------------------------------- | | `firstName`, `lastName`, `fullName` | `companyName`, `companyDomain`, `websiteUrl` | | `email`, `secondaryEmail` | `companyEmail`, `logoUrl` | | `phone`, `mobilePhone` | `industry`, `companySize`, `companyType` | | `jobTitle`, `seniority`, `department`, `jobStartDate` | `employeeCount`, `foundedYear`, `annualRevenue` | | `linkedinUrlProfile`, `twitterUrl` | `linkedinCompanyUrl`, `twitterCompanyUrl` | | `country`, `region`, `city`, `postalCode`, `address`, `timezone` | `notes`, `tags`, `techStack` | | `gender`, `age`, `birthDate`, `language` | | | `yearsOfExperience`, `education`, `skills`, `bio`, `profilePictureUrl` | | * `country`, `city` and the other address keys go to the contact. Use `Company Country`, `Company City` or `Company Address` for the company. * Numeric fields (`age`, `employeeCount`, `annualRevenue`...) drop values that are not numbers. Date fields (`birthDate`, `jobStartDate`) need a valid date, such as `2021-03-01`. * `phone` is the number the list counts and the Phone Finder fills. ## Find a contact Before updating or deleting, look the contact up by email or by LinkedIn URL: [`GET /lists/list/{listId}/rows`](https://docs.emelia.io/docs/lists/list-or-find-contacts.md) **curl** ```bash curl "https://api.emelia.io/lists/list/66f1c0ffee5a7d3b2c4e8a10/rows?email=claire.fontaine@nexora.io" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const url = new URL(`https://api.emelia.io/lists/list/${listId}/rows`); url.searchParams.set('email', 'claire.fontaine@nexora.io'); const res = await fetch(url, { headers: { Authorization: process.env.EMELIA_API_KEY } }); const { items } = await res.json(); const leadId = items[0]?.id; ``` **Python** ```python res = session.get( f"https://api.emelia.io/lists/list/{list_id}/rows", params={"email": "claire.fontaine@nexora.io"}, ) items = res.json()["items"] lead_id = items[0]["id"] if items else None ``` * `email` is matched without case, `linkedinUrl` exactly as stored. Send one of them, not both (`422`). * The lookup covers the list and the lists linked to it, returns up to 50 full contacts, and ignores the pagination. * Each item has an `id`: it is the `leadId` the other routes take. To know which lists contain a person, across your whole account, pass the full email or LinkedIn URL in `leadSearch`: `GET /lists/list?leadSearch=claire.fontaine@nexora.io&page=1&limit=25`. Without `email` or `linkedinUrl`, the same route lists the contacts page by page, see [Pagination](https://docs.emelia.io/docs/pagination.md). In that mode each contact only carries the fields of the columns visible in the list table of the app. ## Update a contact [`PATCH /lists/contacts/{contactId}`](https://docs.emelia.io/docs/lists/update-contact.md) Send only the fields that change. The list is found from the contact, and the campaigns using it get the new values. **curl** ```bash curl -X PATCH https://api.emelia.io/lists/contacts/66f2a1b2c3d4e5f6a7b8c9d0 \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"jobTitle": "VP Engineering", "city": "Lyon"}' ``` **JavaScript** ```js await fetch('https://api.emelia.io/lists/contacts/66f2a1b2c3d4e5f6a7b8c9d0', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ jobTitle: 'VP Engineering', city: 'Lyon' }), }); ``` **Python** ```python session.patch( "https://api.emelia.io/lists/contacts/66f2a1b2c3d4e5f6a7b8c9d0", json={"jobTitle": "VP Engineering", "city": "Lyon"}, ) ``` No duplicate check is made here: changing the email to one already in the list creates a duplicate. ## Delete a contact or a list [`DELETE /lists/list/{listId}/rows/{leadId}`](https://docs.emelia.io/docs/lists/delete-contact.md) [`DELETE /lists/list/{listId}`](https://docs.emelia.io/docs/lists/delete-list.md) Both are permanent: there is no trash and no undo. Deleting a list also deletes its contacts, its companies and its linked lists, and removes them from your campaigns. Deleting a list used as an exclusion list puts the contacts it excluded back into those campaigns. ## Export [`POST /lists/list/export`](https://docs.emelia.io/docs/lists/export-lists-as-csv.md) One list comes back as a CSV file, several lists as a ZIP with one CSV each. The CSV is UTF-8 with BOM, comma separated, with one column per standard field, one per custom variable of your account, then `Lists` and `Campaigns`. **curl** ```bash curl -X POST https://api.emelia.io/lists/list/export \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"listIds": ["66f1c0ffee5a7d3b2c4e8a10"], "async": true}' \ -o export.csv ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/lists/list/export', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ listIds: ['66f1c0ffee5a7d3b2c4e8a10'], async: true }), }); if (res.status === 202) { const { jobId } = await res.json(); // large export: follow the job } else { const csv = await res.text(); // 5,000 contacts or fewer: the file itself } ``` **Python** ```python res = session.post( "https://api.emelia.io/lists/list/export", json={"listIds": ["66f1c0ffee5a7d3b2c4e8a10"], "async": True}, ) if res.status_code == 202: job_id = res.json()["jobId"] # large export: follow the job else: csv = res.text # 5,000 contacts or fewer: the file itself ``` With `async: true`, an export of more than 5,000 contacts answers `202` with a `jobId`: poll [Get the status of an export](https://docs.emelia.io/docs/lists/get-status-of-export.md) every few seconds until `status` is `done`, then call [Download a finished export](https://docs.emelia.io/docs/lists/download-finished-export.md). Up to 5,000 contacts, the file comes back directly. Above 100,000 contacts the export is refused. One asynchronous export runs per account at a time, and jobs are kept 24 hours. ## Attach a list to a campaign [`PATCH /advanced/campaigns/{campaignId}/recipients`](https://docs.emelia.io/docs/campaigns/set-contact-lists.md) ```json { "lists": ["66f1c0ffee5a7d3b2c4e8a10"], "excludedLists": ["66f1c0ffee5a7d3b2c4e8a11"] } ``` `lists` replaces the whole selection: send every list you want attached, not only the new one. Contacts found in an excluded list are left out. This call works while the campaign runs. See [Campaigns](https://docs.emelia.io/docs/campaigns.md#5-attach-the-contacts). New contacts reach the `RUNNING`, `PAUSED` and `DRAFT` campaigns that use the list, usually within seconds and at the latest after the nightly resynchronisation. A `FINISHED` campaign does not start again: reactivate it, or keep it running with `alwaysOn`. To know which campaigns a list feeds, use [Find the campaigns of lists](https://docs.emelia.io/docs/lists/find-campaigns-of-lists.md). --- # List your lists Source: https://docs.emelia.io/docs/lists/list-your-lists Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `GET https://api.emelia.io/lists/list` Returns the contact lists of the account (top-level lists only: lists attached to a parent list are not returned). Pagination is opt-in. Send `page` and/or `limit` to get the paginated envelope `{ items, total, page, pageSize, totalLeads }`. Without both parameters the endpoint returns a bare JSON array with ALL your lists, unpaginated, newest first, and every filter parameter is ignored. Integrations should always send `page` and `limit`. To find which lists contain a person, pass `leadSearch` with a full email address (case-insensitive match) or a LinkedIn profile URL (matched on the profile handle, so trailing slashes and `www` do not matter). Any other value returns zero lists. Each item is the stored list document. Besides the documented fields it can contain internal fields (column preferences, AI action settings, CRM integration settings, linked lists): do not rely on them. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page` | integer | no | Page number, starting at 1. Sending it switches the response to the paginated envelope. Default: `1`. Range: 1 to .... | | `limit` | integer | no | Page size. Values above 100 are clamped to 100. Sending it switches the response to the paginated envelope. Default: `25`. Range: 1 to 100. | | `search` | string | no | Case-insensitive substring of the list name. Paginated mode only. | | `leadSearch` | string | no | Full email address or LinkedIn profile URL: only the lists containing this contact are returned. Paginated mode only. | | `source` | string | no | Import source, exact value, case-insensitive. Values written by the service: `API` (created through this API), `Manual` (empty list created in the app), `CSV` (file import), `Sales Navigator` (LinkedIn scrap), `HubSpot`, `Pipedrive`, `Filter` (list created from a filtered selection). Paginated mode only. | | `status` | enum: `mapped`, `pending mapping` | no | List status. Paginated mode only. | | `tags` | string | no | Comma-separated tag ids: returns the lists carrying at least one of them. Paginated mode only. | | `sortBy` | enum: `name`, `leads`, `emails`, `phones`, `createdAt` | no | Sort field: `leads`, `emails` and `phones` sort on the counters. Unknown values fall back to `createdAt`. Paginated mode only. Default: `"createdAt"`. | | `sortDir` | enum: `asc`, `desc` | no | Sort direction. Any value other than `asc` sorts descending. Default: `"desc"`. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/lists/list" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list', { 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/lists/list", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Paginated envelope when `page` or `limit` is sent, otherwise a bare array of all lists. ```json { "items": [ {} ], "total": 0, "page": 0, "pageSize": 0, "totalLeads": 0 } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Create a list Source: https://docs.emelia.io/docs/lists/create-list Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `POST https://api.emelia.io/lists/list` Creates an empty list with source `API` and status `mapped`, immediately ready to receive contacts through `POST /lists/list/{listId}/contacts` or the batch route. List names do not need to be unique: calling this twice creates two lists, so store the returned `listId`. To make the contacts of the list enter a campaign, attach the list to the campaign (in the app or through the Campaigns API). Contacts added afterwards flow into the campaigns that use the list. Do not send `source`: the value `Manual` is reserved to the web app and starts the list with most columns hidden, which removes their fields from `GET /lists/list/{listId}/rows` until contacts fill them. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | List name, trimmed. Must not be empty. Min length 1. | ```json { "name": "Acme prospects Q4" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/lists/list" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme prospects Q4" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Acme prospects Q4" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/lists/list", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Acme prospects Q4" }, ) print(response.json()) ``` ## Responses ### 201: List created. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `listId` | string | yes | | | `name` | string | yes | | ```json { "success": true, "listId": "66f1c0ffee5a7d3b2c4e8a10", "name": "Acme prospects Q4" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 422: Missing or empty name. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "name is required" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Get a list Source: https://docs.emelia.io/docs/lists/get-list Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `GET https://api.emelia.io/lists/list/{listId}` Returns one list with its counters. The counters are denormalized: `numberOfEmails` and `numberOfPhones` count the contacts that had an email or a `phone` (not `mobilePhone`) when they were added, and are recomputed only at the end of a find-email or find-phone run. They are not updated when a contact is edited: treat them as indicative and use `GET /lists/list/{listId}/rows` for exact data. `enrichmentRun` is non-null while a bulk job runs on the list (find email, find phone, verify email, column normalization, file import, CRM sync). While it is set, starting another bulk job on the list returns 409. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `listId` | string | yes | List id (24-character ObjectId). | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4', { 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/lists/list/65f1a2b3c4d5e6f760f68df4", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: The list. | Field | Type | Required | Description | |---|---|---|---| | `_id` | string | yes | | | `name` | string | yes | | | `fileId` | string or null | no | | | `source` | string | yes | | | `status` | string | yes | | | `companiesOnly` | boolean | yes | | | `numberOfContacts` | integer | yes | | | `numberOfEmails` | integer | yes | | | `numberOfPhones` | integer | yes | | | `numberOfLinkedins` | integer | no | | | `emailFindRunAt` | string or null (date-time) | no | | | `phoneFindRunAt` | string or null (date-time) | no | | | `enrichmentRun` | object or null | yes | | | `hubspotIntegration` | object or null | yes | | | `pipedriveIntegration` | object or null | yes | | | `createdAt` | string (date-time) | yes | | | `updatedAt` | string (date-time) | yes | | ```json { "_id": "66f1c0ffee5a7d3b2c4e8a10", "name": "Acme prospects Q4", "fileId": null, "source": "API", "status": "mapped", "companiesOnly": false, "numberOfContacts": 42, "numberOfEmails": 40, "numberOfPhones": 12, "numberOfLinkedins": 38, "emailFindRunAt": null, "phoneFindRunAt": null, "enrichmentRun": null, "hubspotIntegration": null, "pipedriveIntegration": null, "createdAt": "2026-10-01T09:12:44.000Z", "updatedAt": "2026-10-03T16:40:02.000Z" } ``` ### 400: The list id is not a valid ObjectId. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Invalid list id" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: The list does not exist or belongs to another account. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "List not found" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Rename a list Source: https://docs.emelia.io/docs/lists/rename-list Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `PATCH https://api.emelia.io/lists/list/{listId}` Renames a list. Only `name` can be changed through this route. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `listId` | string | yes | List id. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | New name, trimmed. Min length 1. | ```json { "name": "Acme prospects Q4 (France)" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme prospects Q4 (France)" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Acme prospects Q4 (France)" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Acme prospects Q4 (France)" }, ) print(response.json()) ``` ## Responses ### 200: List renamed. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `_id` | string | yes | | | `name` | string | yes | | ```json { "success": true, "_id": "66f1c0ffee5a7d3b2c4e8a10", "name": "Acme prospects Q4 (France)" } ``` ### 400: Invalid list id, or missing or empty name (note: 400 here, while list creation answers 422 for the same mistake). | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: The list does not exist or belongs to another account. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "List not found" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Delete a list Source: https://docs.emelia.io/docs/lists/delete-list Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `DELETE https://api.emelia.io/lists/list/{listId}` Permanently deletes the list, every contact and company row it contains, and its child or linked lists. The deleted list and its contacts are removed from the recipients of your campaigns, which are then resynchronized: deleting a list used as an exclusion list puts the contacts it was excluding back into the campaigns. This cannot be undone. The call is synchronous: deleting a very large list can take a while. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `listId` | string | yes | List id. | ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: List deleted, with deletion counters. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `deletedLists` | integer | yes | The list plus its linked lists. | | `deletedContacts` | integer | yes | | | `deletedCompanies` | integer | yes | | | `campaignsUpdated` | integer | yes | Campaigns whose recipients were updated. | ```json { "success": true, "deletedLists": 1, "deletedContacts": 42, "deletedCompanies": 40, "campaignsUpdated": 1 } ``` ### 400: The list id is not a valid ObjectId. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Invalid list id" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: The list does not exist or belongs to another account. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "List not found" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Export lists as CSV Source: https://docs.emelia.io/docs/lists/export-lists-as-csv Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `POST https://api.emelia.io/lists/list/export` Exports one list as a CSV file, or several lists as a ZIP archive with one CSV per list. CSV format: UTF-8 with BOM, comma separator, CRLF line endings. Columns: `First Name`, `Last Name`, `Full Name`, `Email`, `Phone`, `Mobile Phone`, `Job Title`, `LinkedIn URL`, `Bio`, `Company`, `Website`, `LinkedIn Page`, `Company Size`, `Industry`, `Country`, `Region`, `City`, `Status`, `Handle`, then one column per custom variable of the account (display name), then `Lists` and `Campaigns` (one `Campaign name - STATUS` line per campaign). Company-only lists use a shorter header (`Company`, `Website`, `LinkedIn Page`, `Company Size`, `Industry`, custom variables, `Lists`). `leadIds`, `search`, `statuses` and `filters` only apply when exactly one list is exported; they are ignored for a multi-list ZIP. List ids that are not yours are silently skipped. **Large exports**: send `async: true`. If the export covers more than 5,000 contacts, the response is `202` with a `jobId`: poll `GET /lists/list/export/jobs/{jobId}` until `status` is `done`, then download the file. At or under 5,000 contacts the file is returned directly (200) even with `async: true`, so handle both responses. With `async: true`, an export above 100,000 contacts is refused (422). Only one asynchronous export runs per account: while one is queued or running, a new request returns that existing job (it may concern other lists). Without `async`, the file is always streamed synchronously, whatever its size, and the connection may be cut after 120 seconds of silence. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `listIds` | array of string | yes | Lists to export, in the order of the ZIP. Invalid ids and lists that are not yours are skipped. | | `leadIds` | array of string | no | Export only these contacts (single-list export only). | | `search` | string | no | Same as `search` of the rows route (single-list export only). | | `statuses` | array of string | no | Engagement statuses, `NO_ACTIVITY` included (single-list export only). | | `filters` | array of object | no | Column filters (single-list export only). Max 10 items. | | `filters[].column` | string | yes | Contact columns: `firstName`, `lastName`, `fullName`, `jobTitle`, `email`, `secondaryEmail`, `phone`, `mobilePhone`, `linkedinUrlProfile`, `twitterUrl`, `country`, `city`, `seniority`, `department`, `bio`, `gender`, `emailVerified`. Company columns: `companyName`, `companyDomain`, `websiteUrl`, `industry`, `linkedinCompanyUrl`, `companyCountry`, `companyCity`. Custom variables: `custom`. | | `filters[].operator` | enum: `equals`, `not_equals`, `contains`, `not_contains`, `empty`, `not_empty` | yes | | | `filters[].value` | string | no | Required except for `empty` and `not_empty`. | | `async` | boolean | no | Allow an asynchronous job (202) when the export covers more than 5,000 contacts. Default: `false`. | ```json { "listIds": [ "string" ], "leadIds": [ "string" ], "search": "string", "statuses": [ "string" ], "filters": [ { "column": "string", "operator": "equals", "value": "string" } ], "async": false } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/lists/list/export" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "listIds": [ "string" ], "leadIds": [ "string" ], "search": "string", "statuses": [ "string" ], "filters": [ { "column": "string", "operator": "equals", "value": "string" } ], "async": false }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/export', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "listIds": [ "string" ], "leadIds": [ "string" ], "search": "string", "statuses": [ "string" ], "filters": [ { "column": "string", "operator": "equals", "value": "string" } ], "async": false }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/lists/list/export", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "listIds": [ "string" ], "leadIds": [ "string" ], "search": "string", "statuses": [ "string" ], "filters": [ { "column": "string", "operator": "equals", "value": "string" } ], "async": False }, ) print(response.json()) ``` ## Responses ### 200: The file: `text/csv; charset=utf-8` named after the list for one list, `application/zip` named `emelia-lists-export.zip` for several lists. Sent as an attachment (`Content-Disposition`). Content type: `text/csv`. ### 202: Asynchronous export accepted (only with `async: true` and more than 5,000 contacts). `total` is the number of contacts of this request; if an export was already running, `jobId` and `status` are those of that running export. | Field | Type | Required | Description | |---|---|---|---| | `jobId` | string (uuid) | yes | | | `status` | enum: `queued`, `running`, `done`, `failed` | yes | | | `total` | integer | yes | Contacts covered by this request (estimate based on the list counters). | ```json { "jobId": "0b6f3c1e-8a2d-4c5b-9e7f-1a2b3c4d5e6f", "status": "queued", "total": 18250 } ``` ### 400: No valid list id. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "listIds is required" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: None of the requested lists is yours. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "No matching list found" } ``` ### 422: Invalid `filters`, or export too large (with `async: true`). | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 500: Export failed before the file started. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Export failed", "error": "List not found" } ``` ### 503: Asynchronous export temporarily unavailable (job storage down). Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Async export unavailable" } ``` --- # Find the campaigns using lists Source: https://docs.emelia.io/docs/lists/find-campaigns-of-lists Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `GET https://api.emelia.io/lists/list/campaign-usage` For each list id, returns the non-archived campaigns that include the list in their recipients. Use it to know whether adding contacts to a list will feed a campaign. Lists used only as exclusion lists are not reported. Ids that are not valid ObjectIds are dropped; ids of lists that are not yours return an empty array. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `listIds` | string | yes | Comma-separated list ids. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/lists/list/campaign-usage?listIds=66f1c0ffee5a7d3b2c4e8a10%2C66f1c0ffee5a7d3b2c4e8a11" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/campaign-usage?listIds=66f1c0ffee5a7d3b2c4e8a10%2C66f1c0ffee5a7d3b2c4e8a11', { 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/lists/list/campaign-usage?listIds=66f1c0ffee5a7d3b2c4e8a10%2C66f1c0ffee5a7d3b2c4e8a11", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Campaigns per list id. | Field | Type | Required | Description | |---|---|---|---| | `usage` | object | yes | Keys are the requested list ids. | ```json { "usage": { "66f1c0ffee5a7d3b2c4e8a10": [ { "_id": "66e0d9c8b7a6f5e4d3c2b1a0", "name": "Acme Q4 outreach", "status": "RUNNING" } ], "66f1c0ffee5a7d3b2c4e8a11": [] } } ``` ### 400: `listIds` missing or empty. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "listIds query param is required" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # List or find the contacts of a list Source: https://docs.emelia.io/docs/lists/list-or-find-contacts Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `GET https://api.emelia.io/lists/list/{listId}/rows` Two modes. **Exact lookup** (use it before an update or a delete): pass `email` OR `linkedinUrl`. The email is matched exactly but case-insensitively, the LinkedIn URL is compared as an exact string (a trailing slash or a missing `www` is a different URL). The search covers this list and the lists linked to it, returns up to 50 matches, ignores `search`, filters and pagination, and returns the full contact objects. The envelope then has `page: 1`, `pageSize` equal to the number of items (1 when empty) and no `customVariables`. **Paginated listing**: without `email` and `linkedinUrl`, returns one page of the contacts stored in this list (not in linked lists), in creation order unless `sortBy` is set. Company rows without a contact (company-only lists) are included with `rowKind: "company_only"` and an id of the form `co:` that the contact routes do not accept. In paginated mode, when the list has column settings (every list created by this API or by the current app has them), each item only contains the fields of the columns visible in the list table of the app: a column hidden in the app disappears from the API response, `labels` is not returned and `company` carries no `companyId`. Use the exact lookup to get the full object. Deep pages are expensive (the service sorts `page x pageSize` rows to serve one page): page through large lists with `pageSize=100` and avoid restarting from page 1, or use the export. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `listId` | string | yes | List id. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `email` | string (email) | no | Exact lookup by email (case-insensitive). Mutually exclusive with `linkedinUrl`. | | `linkedinUrl` | string | no | Exact lookup by LinkedIn profile URL (compared verbatim with the stored `linkedinUrlProfile`). Mutually exclusive with `email`. | | `page` | integer | no | Page number. Invalid or negative values fall back to 1. Default: `1`. Range: 1 to .... | | `pageSize` | integer | no | Page size, clamped between 1 and 100. Default: `50`. Range: 1 to 100. | | `search` | string | no | Free-text search, 200 characters max. A full email address is matched exactly (case-insensitive). Otherwise every word must appear (case-insensitive substring) in one of `firstName`, `lastName`, `fullName`, `email` or `linkedinUrlProfile`; company-only rows are matched on the company name. | | `sortBy` | enum: `firstName`, `lastName`, `fullName`, `jobTitle`, `email`, `phone`, `linkedinUrlProfile`, `country`, `city`, `createdAt`, `updatedAt` | no | Sort field. Text fields sort case- and accent-insensitively. Any other value is ignored (creation order). | | `sortDir` | enum: `asc`, `desc` | no | Sort direction, used with `sortBy`. Default: `"asc"`. | | `statuses` | string | no | Comma-separated engagement statuses (as stored on the contacts, for example `SENT`, `OPENED`, `MAIL_REPLIED`, `ACCEPTED`). The token `NO_ACTIVITY` matches contacts without status. Up to 30 values. Company-only rows are excluded when this filter is set. | | `filters` | string | no | URL-encoded JSON array of column rules (up to 10), all combined with AND. See `ListsFilterRule`. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/rows" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/rows', { 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/lists/list/65f1a2b3c4d5e6f760f68df4/rows", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Contacts of the list. | Field | Type | Required | Description | |---|---|---|---| | `items` | array of object | yes | | | `items[].id` | string | yes | Contact id, or `co:` for a company-only row. | | `items[].rowKind` | enum: `lead`, `company_only` | yes | | | `items[].firstName` | string | no | | | `items[].lastName` | string | no | | | `items[].fullName` | string | no | For a company-only row: the company name, or `-`. | | `items[].jobTitle` | string | no | | | `items[].gender` | string or null | no | Free text as imported. | | `items[].email` | string or null | no | | | `items[].secondaryEmail` | string or null | no | | | `items[].phone` | string or null | no | | | `items[].mobilePhone` | string or null | no | | | `items[].linkedinUrlProfile` | string or null | no | | | `items[].twitterUrl` | string or null | no | | | `items[].country` | string | no | | | `items[].city` | string | no | | | `items[].seniority` | string | no | Known levels are returned with spaces (for example `C LEVEL`). | | `items[].department` | string | no | | | `items[].bio` | string | no | | | `items[].profilePictureUrl` | string or null | no | | | `items[].emailVerified` | enum: `verified`, `unverified`, `invalid`, `unknown` | no | Result of the last email verification; `unknown` when never verified. | | `items[].status` | string | no | Latest engagement status written by the campaigns (for example `SENT`, `OPENED`, `MAIL_REPLIED`, `ACCEPTED`), empty when none. | | `items[].company` | object or null | no | | | `items[].labels` | array of object | no | Always empty. Not returned in the paginated listing. | | `items[].customFields` | array of object | no | | | `items[].createdAt` | string | no | ISO date, or empty string. | | `items[].updatedAt` | string | no | ISO date, or empty string. | | `total` | integer | yes | Total matching rows (paginated mode) or number of matches (exact lookup). | | `page` | integer | yes | | | `pageSize` | integer | yes | | | `customVariables` | array of object | no | Custom variables shown in this list or used by the returned rows. Absent in the exact lookup mode. | | `customVariables[].technicalName` | string | yes | | | `customVariables[].displayName` | string | yes | | | `customVariables[].schema` | enum: `lead`, `company` | yes | | ```json { "items": [ { "id": "string", "rowKind": "lead", "firstName": "string", "lastName": "string", "fullName": "string", "jobTitle": "string", "gender": "string", "email": "string", "secondaryEmail": "string", "phone": "string", "mobilePhone": "string", "linkedinUrlProfile": "string", "twitterUrl": "string", "country": "string", "city": "string", "seniority": "string", "department": "string", "bio": "string", "profilePictureUrl": "string", "emailVerified": "verified", "status": "string", "company": {}, "labels": [], "customFields": [], "createdAt": "string", "updatedAt": "string" } ], "total": 0, "page": 0, "pageSize": 0, "customVariables": [ { "technicalName": "string", "displayName": "string", "schema": "lead" } ] } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: The list does not exist or belongs to another account. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "List not found" } ``` ### 422: `email` and `linkedinUrl` sent together, or invalid `filters`. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Delete a contact Source: https://docs.emelia.io/docs/lists/delete-contact Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `DELETE https://api.emelia.io/lists/list/{listId}/rows/{leadId}` Permanently deletes a contact and its company row. The contact can live in the list or in a list linked to it. It is removed from the campaigns that reference it, the list counters are decremented and the campaigns using the list are resynchronized. This cannot be undone. A second call for the same contact returns 404. Find the `leadId` with the exact lookup of `GET /lists/list/{listId}/rows`. Company-only rows (`co:` ids) cannot be deleted with this route. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `listId` | string | yes | List id. | | `leadId` | string | yes | Contact id. | ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/rows/65f1a2b3c4d5e6f7af1c3d42" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/rows/65f1a2b3c4d5e6f7af1c3d42', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/rows/65f1a2b3c4d5e6f7af1c3d42", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Contact deleted. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid list id or contact id. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Invalid id" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: List not found (or not yours), or contact not found in this list or its linked lists. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Add a contact to a list (add or update) Source: https://docs.emelia.io/docs/lists/add-contact Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `POST https://api.emelia.io/lists/list/{listId}/contacts` Adds ONE contact to a list from a flat JSON object: field names as keys, scalar values (string, number or boolean). This is the endpoint for automations (Zapier, Make, n8n, scripts) and the standard way to feed a running campaign: if campaigns in RUNNING, PAUSED or DRAFT status use the list, the contact is propagated to them asynchronously (usually within seconds, at the latest by the nightly reconciliation). Adding contacts consumes no credits. For more than one contact, use the batch route. ### How keys are resolved Each key is resolved in this order: (1) case-insensitively against the recognized names of the standard fields (`First Name`, `Prénom`, `email_address`, `Company`...); (2) against the standard field keys, ignoring case, spaces, `_` and `-` (`firstName`, `first_name` and `FIRSTNAME` all work), and also as a prefix or suffix of a field key when the key has 3 characters or more; (3) against your existing custom variables (technical or display name); (4) a key whose name ends in `id` is never matched by its value and becomes a custom variable; (5) otherwise the VALUE is inspected: an email-looking value goes to `email`, 6 digits or more go to `phone`, a `linkedin.com/in/` URL goes to `linkedinUrlProfile`, any other `http(s)://` URL goes to the company `websiteUrl`. Only when nothing matched is a new lead custom variable created (display name = the key, technical name derived from it) and reported in `createdCustomVariables`. ### Pitfalls - Use the exact keys listed below. Short or generic keys are fuzzy-matched: `job` lands in `jobStartDate` (a date, the request fails), `last` in `lastContacted` (the value is lost), `year` in `yearsOfExperience`, `date` in `birthDate`, `note` in the company `notes`, `tag` in the company `tags`. - An unknown key whose value looks like a phone number (company registration number, order number, a date such as `2024-01-15`) is stored in `phone`, not in a new custom variable. Declare the custom variable first with `POST /lists/custom-variable`: an existing variable is matched before the value is inspected. - `mobile` maps to `phone`. Use `mobilePhone` for a mobile number. - A standard field takes at most one key per contact. When two keys target the same field, the second one moves to the next matching field (a second email key can end up in `emailVerified` and be lost). - Numeric fields (`age`, `yearsOfExperience`, `employeeCount`, `foundedYear`, `annualRevenue`) silently drop non-numeric values. Date fields (`birthDate`, `jobStartDate`) must be valid dates (use ISO 8601, for example `2021-03-01`): an unparsable date makes the request fail. - `null` and empty strings are ignored: this endpoint cannot clear a field. Arrays and objects are rejected (422). - Every unknown key creates an account-wide custom variable that cannot be deleted through the API. ### Standard keys Contact: `firstName`, `lastName`, `fullName`, `email`, `secondaryEmail`, `phone`, `mobilePhone`, `linkedinUrlProfile`, `twitterUrl`, `jobTitle`, `seniority`, `department`, `gender`, `age`, `birthDate`, `language`, `country`, `region`, `city`, `postalCode`, `address`, `timezone`, `jobStartDate`, `yearsOfExperience`, `education`, `skills` (list separated by commas, semicolons or pipes), `bio`, `profilePictureUrl`. Company: `companyName`, `companyDomain`, `companyEmail`, `websiteUrl`, `linkedinCompanyUrl`, `twitterCompanyUrl`, `logoUrl`, `industry`, `companySize`, `companyType`, `employeeCount`, `foundedYear`, `annualRevenue`, `notes`, `tags`, `techStack`. The keys `country`, `city`, `region`, `postalCode`, `address` and `timezone` go to the contact: use `Company Country`, `Company City` or `Company Address` to fill the company ones. ### Duplicates and idempotency Before creating, the service looks in this list and the lists linked to it for a contact with the same `email` (exact comparison, case-sensitive), then with the same `linkedinUrlProfile` (exact string). If one is found, nothing is created: the response returns its `leadId` with `duplicate: true`. With `updateIfExists=true` the payload is applied to that contact instead (sent fields overwrite, absent fields are kept, identity fields included) and the response has `updated: true`. A contact without email and without LinkedIn URL is never deduplicated: retrying such a call creates a new contact each time. Send emails in lowercase and LinkedIn URLs in one canonical form. The status code is 200 for a creation, an ignored duplicate and an update alike: read `duplicate` and `updated`. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `listId` | string | yes | List id. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `updateIfExists` | enum: `true`, `false` | no | Only the exact string `true` enables it. When a duplicate is found, apply the payload to the existing contact instead of ignoring it. Query parameter, never a body key (a body key would be stored as a contact field). Default: `"false"`. | ## Request body Content type: `application/json`, required. Flat contact object: one key per field, scalar values only. Standard keys are listed in the description of `POST /lists/list/{listId}/contacts`; any other key goes to an existing custom variable of the same name or creates a new one. Numbers and booleans are converted to strings. `null` and empty strings are ignored. Arrays and objects are rejected. ```json { "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "jobTitle": "Head of Sales", "companyName": "Acme", "websiteUrl": "https://www.acme.com", "icebreaker": "Loved your talk at SaaStock" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/contacts" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "jobTitle": "Head of Sales", "companyName": "Acme", "websiteUrl": "https://www.acme.com", "icebreaker": "Loved your talk at SaaStock" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/contacts', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "jobTitle": "Head of Sales", "companyName": "Acme", "websiteUrl": "https://www.acme.com", "icebreaker": "Loved your talk at SaaStock" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/contacts", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "jobTitle": "Head of Sales", "companyName": "Acme", "websiteUrl": "https://www.acme.com", "icebreaker": "Loved your talk at SaaStock" }, ) print(response.json()) ``` ## Responses ### 200: Contact created, duplicate ignored, or duplicate updated. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `leadId` | string | yes | Id of the created contact, or of the existing one when `duplicate` is true. | | `companyId` | string or null | yes | Id of the company row, null when the contact has none. | | `duplicate` | boolean | yes | True when a contact with the same email or LinkedIn URL already existed: nothing was created. | | `updated` | boolean | yes | True when that existing contact was updated (`updateIfExists=true`). | | `createdCustomVariables` | array of object | yes | | | `createdCustomVariables[].technicalName` | string | yes | | | `createdCustomVariables[].displayName` | string | yes | The key as sent. | | `createdCustomVariables[].schema` | enum: `lead`, `company` | yes | Always `lead` for variables created by the contact routes. | ```json { "success": true, "leadId": "string", "companyId": "string", "duplicate": true, "updated": true, "createdCustomVariables": [ { "technicalName": "string", "displayName": "string", "schema": "lead" } ] } ``` ### 400: The list id is not a valid ObjectId. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Invalid list id" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: The list does not exist or belongs to another account. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "List not found" } ``` ### 413: JSON body larger than 5 MB. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Request body too large: the limit is 5mb", "code": "PAYLOAD_TOO_LARGE", "params": { "limit": "5mb" } } ``` ### 422: Invalid body. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 500: Unexpected error, for example an invalid date in `birthDate` or `jobStartDate`. The message is the raw internal message. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` --- # Add up to 100 contacts to a list Source: https://docs.emelia.io/docs/lists/add-contacts-in-bulk Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `POST https://api.emelia.io/lists/list/{listId}/contacts/batch` Adds 1 to 100 contacts in one call. Each item of `contacts` is the same flat object as in `POST /lists/list/{listId}/contacts`, with the same key resolution, custom variable creation, duplicate rules and `updateIfExists` behaviour (read that endpoint first). Rows are processed one after the other and independently: a failing row is reported in `results` with its `index` and an `error` message and does not stop the others. A row repeating an email or LinkedIn URL already seen earlier in the same batch is counted as a duplicate of that earlier row and is never updated, even with `updateIfExists=true`. Counters: `created + duplicates + failed = total`, and `updated` is a subset of `duplicates`. The body must be an object with a `contacts` array, not a bare array. Split larger loads into several calls of 100. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `listId` | string | yes | List id. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `updateIfExists` | enum: `true`, `false` | no | Only the exact string `true` enables it. Applies to every row. Default: `"false"`. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `contacts` | array of object | yes | Max 100 items. | ```json { "contacts": [ { "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "companyName": "Acme" }, { "firstName": "Jane", "lastName": "Roe", "email": "jane.roe@acme.com", "companyName": "Acme", "icebreaker": "Congrats on the new office" } ] } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/contacts/batch" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contacts": [ { "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "companyName": "Acme" }, { "firstName": "Jane", "lastName": "Roe", "email": "jane.roe@acme.com", "companyName": "Acme", "icebreaker": "Congrats on the new office" } ] }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/contacts/batch', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "contacts": [ { "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "companyName": "Acme" }, { "firstName": "Jane", "lastName": "Roe", "email": "jane.roe@acme.com", "companyName": "Acme", "icebreaker": "Congrats on the new office" } ] }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/lists/list/65f1a2b3c4d5e6f760f68df4/contacts/batch", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "contacts": [ { "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "companyName": "Acme" }, { "firstName": "Jane", "lastName": "Roe", "email": "jane.roe@acme.com", "companyName": "Acme", "icebreaker": "Congrats on the new office" } ] }, ) print(response.json()) ``` ## Responses ### 200: Batch processed. Read `results` for the outcome of each row. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | True even when some rows failed. | | `total` | integer | yes | | | `created` | integer | yes | | | `duplicates` | integer | yes | Includes the updated duplicates. | | `updated` | integer | yes | | | `failed` | integer | yes | | | `results` | array of object | yes | One entry per input row, in input order. | | `results[].index` | integer | yes | Position of the row in `contacts`, starting at 0. | | `results[].leadId` | string | no | | | `results[].companyId` | string or null | no | | | `results[].duplicate` | boolean | no | | | `results[].updated` | boolean | no | | | `results[].error` | string | no | | | `createdCustomVariables` | array of object | yes | | | `createdCustomVariables[].technicalName` | string | yes | | | `createdCustomVariables[].displayName` | string | yes | The key as sent. | | `createdCustomVariables[].schema` | enum: `lead`, `company` | yes | Always `lead` for variables created by the contact routes. | ```json { "success": true, "total": 3, "created": 1, "duplicates": 1, "updated": 0, "failed": 1, "results": [ { "index": 0, "leadId": "66f2a1b2c3d4e5f6a7b8c9d0", "companyId": "66f2a1b2c3d4e5f6a7b8c9e0", "duplicate": true, "updated": false }, { "index": 1, "leadId": "66f2a1b2c3d4e5f6a7b8c9d1", "companyId": "66f2a1b2c3d4e5f6a7b8c9e1", "duplicate": false, "updated": false }, { "index": 2, "error": "Field \"tags\" must be a string, number or boolean" } ], "createdCustomVariables": [] } ``` ### 400: The list id is not a valid ObjectId. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Invalid list id" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: The list does not exist or belongs to another account. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "List not found" } ``` ### 413: JSON body larger than 5 MB. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Request body too large: the limit is 5mb", "code": "PAYLOAD_TOO_LARGE", "params": { "limit": "5mb" } } ``` ### 422: `contacts` missing, not an array, empty or longer than 100. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Update a contact Source: https://docs.emelia.io/docs/lists/update-contact Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `PATCH https://api.emelia.io/lists/contacts/{contactId}` Updates a contact identified by its id only (its list is derived from the contact). The body is the same flat object as `POST /lists/list/{listId}/contacts`, with the same key resolution and the same pitfalls: unknown keys create custom variables, `null` and empty strings are ignored (a field cannot be cleared), sent fields overwrite, absent fields stay untouched. A company row is created if the contact has none and company fields are sent. No duplicate check is made: changing the email to one already present in the list creates a duplicate. List counters are not updated. The campaigns using the list are resynchronized. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `contactId` | string | yes | Contact id (`leadId`). | ## Request body Content type: `application/json`, required. Flat contact object: one key per field, scalar values only. Standard keys are listed in the description of `POST /lists/list/{listId}/contacts`; any other key goes to an existing custom variable of the same name or creates a new one. Numbers and booleans are converted to strings. `null` and empty strings are ignored. Arrays and objects are rejected. ```json { "jobTitle": "VP Sales", "phone": "+33612345678", "icebreaker": "Saw your post about the Series A" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/lists/contacts/65f1a2b3c4d5e6f74db757ce" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "jobTitle": "VP Sales", "phone": "+33612345678", "icebreaker": "Saw your post about the Series A" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/contacts/65f1a2b3c4d5e6f74db757ce', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "jobTitle": "VP Sales", "phone": "+33612345678", "icebreaker": "Saw your post about the Series A" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/lists/contacts/65f1a2b3c4d5e6f74db757ce", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "jobTitle": "VP Sales", "phone": "+33612345678", "icebreaker": "Saw your post about the Series A" }, ) print(response.json()) ``` ## Responses ### 200: Contact updated. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `leadId` | string | yes | | | `companyId` | string or null | yes | | | `createdCustomVariables` | array of object | yes | | | `createdCustomVariables[].technicalName` | string | yes | | | `createdCustomVariables[].displayName` | string | yes | The key as sent. | | `createdCustomVariables[].schema` | enum: `lead`, `company` | yes | Always `lead` for variables created by the contact routes. | ```json { "success": true, "leadId": "66f2a1b2c3d4e5f6a7b8c9d0", "companyId": "66f2a1b2c3d4e5f6a7b8c9e0", "createdCustomVariables": [] } ``` ### 400: The contact id is not a valid ObjectId. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Invalid contact id" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: Contact not found, deleted, or not yours. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Contact not found" } ``` ### 413: JSON body larger than 5 MB. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Request body too large: the limit is 5mb", "code": "PAYLOAD_TOO_LARGE", "params": { "limit": "5mb" } } ``` ### 422: Invalid body (same messages as the add route). | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Contact has no usable field" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Create a custom variable Source: https://docs.emelia.io/docs/lists/create-custom-variable Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `POST https://api.emelia.io/lists/custom-variable` Declares a custom variable on the account, so that contact keys with this name (technical or display name) land in it instead of being guessed. Declare your variables before loading contacts. This is the only way to create a COMPANY custom variable: the contact routes only create contact (`lead`) variables. Naming rule: a technical name that is already a clean identifier (letter first, then letters, digits or `_`) is kept, with its first letter lowercased (`basile_gmb_name` stays `basile_gmb_name`). Anything else is camelCased with accents and symbols dropped (`Ice breaker` becomes `iceBreaker`). The stored name is returned. The call is refused (422) when a variable with the same name already exists for this schema (comparison ignoring case, spaces, `_` and `-`) or when the name is a standard field of the schema: it is therefore safe to call it every time and treat 422 as already existing. Variables are shared by all your lists and cannot be renamed or deleted through the API. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `technicalName` | string | yes | Name used as the contact key and in `{{variables}}`. Normalized (see the route description). Min length 1. | | `schema` | enum: `lead`, `company` | yes | `lead`: stored on the contact. `company`: stored on the company row of the contact. | | `displayName` | string | no | Label shown in the app. Defaults to the technical name. | ```json { "technicalName": "icebreaker", "schema": "lead", "displayName": "Icebreaker" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/lists/custom-variable" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "technicalName": "icebreaker", "schema": "lead", "displayName": "Icebreaker" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/custom-variable', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "technicalName": "icebreaker", "schema": "lead", "displayName": "Icebreaker" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/lists/custom-variable", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "technicalName": "icebreaker", "schema": "lead", "displayName": "Icebreaker" }, ) print(response.json()) ``` ## Responses ### 201: Variable created. | Field | Type | Required | Description | |---|---|---|---| | `technicalName` | string | yes | Name as stored after normalization. | | `displayName` | string | yes | | | `schema` | enum: `lead`, `company` | yes | | ```json { "technicalName": "icebreaker", "displayName": "Icebreaker", "schema": "lead" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 422: Invalid body, empty name after normalization, existing variable or standard field. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Check whether a custom variable exists Source: https://docs.emelia.io/docs/lists/check-custom-variable Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `GET https://api.emelia.io/lists/custom-variable/check` Tells whether a name is already taken for a schema, either by one of your custom variables (`reason: custom_variable`) or by a standard field (`lead_field` or `company_field`). The name is normalized with the same rule as the creation route and the normalized `technicalName` is returned. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `technicalName` | string | yes | Name to check. | | `schema` | enum: `lead`, `company` | yes | `lead` for contact variables, `company` for company variables. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/lists/custom-variable/check?technicalName=industryFocus&schema=lead" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/custom-variable/check?technicalName=industryFocus&schema=lead', { 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/lists/custom-variable/check?technicalName=industryFocus&schema=lead", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Check result. | Field | Type | Required | Description | |---|---|---|---| | `exists` | boolean | yes | | | `reason` | enum: `custom_variable`, `lead_field`, `company_field` | no | Present when `exists` is true. | | `technicalName` | string | yes | The normalized name that was checked. | ```json { "exists": true, "reason": "custom_variable", "technicalName": "string" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 422: Missing or invalid query parameter. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "\"schema\" must be one of [lead, company]" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Get the status of an export Source: https://docs.emelia.io/docs/lists/get-status-of-export Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `GET https://api.emelia.io/lists/list/export/jobs/{jobId}` Returns the progress of an asynchronous export started with `POST /lists/list/export` and `async: true`. Poll every few seconds until `status` is `done` (then call the download route) or `failed` (read `error`). Jobs are kept 24 hours; after that, or for a job of another account, the route returns 404. A job interrupted by a service restart ends as `failed`: start the export again. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `jobId` | string (uuid) | yes | Job id returned by the export request (UUID). | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/lists/list/export/jobs/0b6f3c1e-8a2d-4c5b-9e7f-1a2b3c4d5e6f" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/export/jobs/0b6f3c1e-8a2d-4c5b-9e7f-1a2b3c4d5e6f', { 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/lists/list/export/jobs/0b6f3c1e-8a2d-4c5b-9e7f-1a2b3c4d5e6f", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Job state. | Field | Type | Required | Description | |---|---|---|---| | `jobId` | string (uuid) | yes | | | `status` | enum: `queued`, `running`, `done`, `failed` | yes | | | `rows` | integer | yes | Rows written so far. | | `total` | integer or null | yes | Rows to write, null until counted. | | `filename` | string or null | yes | File name, set when done. | | `error` | string or null | yes | Failure reason when `status` is `failed`. | ```json { "jobId": "string", "status": "queued", "rows": 0, "total": 0, "filename": "string", "error": "string" } ``` ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: Unknown, expired or foreign job. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Export job not found" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Download a finished export Source: https://docs.emelia.io/docs/lists/download-finished-export Guide: [Lists & contacts](https://docs.emelia.io/docs/lists.md) `GET https://api.emelia.io/lists/list/export/jobs/{jobId}/download` Streams the file of a finished asynchronous export (CSV for one list, ZIP for several), as an attachment. Returns 409 while the job is not `done`. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `jobId` | string (uuid) | yes | Job id (UUID). | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/lists/list/export/jobs/0b6f3c1e-8a2d-4c5b-9e7f-1a2b3c4d5e6f/download" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/lists/list/export/jobs/0b6f3c1e-8a2d-4c5b-9e7f-1a2b3c4d5e6f/download', { 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/lists/list/export/jobs/0b6f3c1e-8a2d-4c5b-9e7f-1a2b3c4d5e6f/download", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: The export file. Content type: `text/csv`. ### 401: Missing, unknown or disabled API key, or the account of the key no longer exists. Note the body shape: `error` and `code`, not `message`. | Field | Type | Required | Description | |---|---|---|---| | `error` | string | yes | | | `code` | enum: `AUTH_REQUIRED`, `ACCOUNT_NOT_FOUND` | yes | | ```json { "error": "string", "code": "AUTH_REQUIRED" } ``` ### 404: Unknown, expired or foreign job, or file missing. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "string", "code": "string", "params": {}, "error": "string" } ``` ### 409: The export is not finished. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Export not ready", "status": "running" } ``` ### 500: Unexpected server error. `error`, when present, is a raw internal message: do not parse it. Retry later. | Field | Type | Required | Description | |---|---|---|---| | `message` | string | yes | English, human-readable message. | | `code` | string | no | Stable error code, when available (for example `PAYLOAD_TOO_LARGE`, `INVALID_JSON`, `export_too_large`, `companies_only`, `enrichment_running`). | | `params` | object | no | | | `error` | string | no | Raw internal message (unexpected errors only). | ```json { "message": "Hmm... Something went wrong. Please try again later." } ``` --- # Enrichment > Find professional emails, find phone numbers and verify addresses with the Emelia enrichment API. Every call starts a job that you poll until it is done. Source: https://docs.emelia.io/docs/enrichment Emelia has three enrichment tools, each with its guide and its routes: - [Email Finder API](https://docs.emelia.io/docs/enrichment/email-finder): The professional email of a person, from a name and a company. 1 credit, refunded when nothing is found. - [Phone Number Finder API](https://docs.emelia.io/docs/enrichment/phone-finder): The phone number of a person, from a LinkedIn profile URL. 50 credits, refunded when nothing is found. - [Email Verification API](https://docs.emelia.io/docs/enrichment/email-verifier): Whether an address can receive mail. 0.25 credit, never refunded. ## How a job works The three tools work the same way: 1. You `POST` the input. Emelia answers at once with `{ "success": true, "jobId": "..." }`. 2. You `GET` the job every 3 to 5 seconds until its `status` is no longer `running`. 3. You read the result in `data`: `status` is `done` (or `error`), with the email, the number or the verdict. | Tool | Start | Result | History | Typical duration | | -------------- | -------------------------- | --------------------------------- | ------------------------- | ------------------------------ | | Email Finder | `POST /tools/find/email` | `GET /tools/find/email/{jobId}` | `GET /tools/find/email` | Seconds, up to about 4 minutes | | Phone Finder | `POST /tools/find/phone` | `GET /tools/find/phone/{jobId}` | `GET /tools/find/phone` | No fixed upper bound | | Email Verifier | `POST /tools/verify/email` | `GET /tools/verify/email/{jobId}` | `GET /tools/verify/email` | Under 60 seconds, then `error` | This helper starts a job and polls it, for any of the three tools: **curl** ```bash # Start the job JOB_ID=$(curl -s -X POST https://api.emelia.io/tools/verify/email \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "claire.fontaine@nexora.io"}' | jq -r .jobId) # Poll it every 3 seconds until it is no longer running while true; do RESULT=$(curl -s "https://api.emelia.io/tools/verify/email/$JOB_ID" -H "Authorization: $EMELIA_API_KEY") [ "$(echo "$RESULT" | jq -r .data.status)" != "running" ] && break sleep 3 done echo "$RESULT" ``` **JavaScript** ```js const API = 'https://api.emelia.io'; const headers = { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /** Starts a job, then polls it every 3 seconds, for up to 5 minutes. */ export async function runJob(path, input) { const start = await fetch(`${API}${path}`, { method: 'POST', headers, body: JSON.stringify(input) }); const { jobId, error } = await start.json(); if (!jobId) throw new Error(error ?? `HTTP ${start.status}`); for (let i = 0; i < 100; i++) { await sleep(3000); const res = await fetch(`${API}${path}/${jobId}`, { headers }); const { data } = await res.json(); if (data && data.status !== 'running') return { jobId, ...data }; } return { jobId, status: 'running' }; // keep the jobId and come back later } console.log(await runJob('/tools/verify/email', { email: 'claire.fontaine@nexora.io' })); ``` **Python** ```python import os import time import requests API = "https://api.emelia.io" session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] def run_job(path, payload, every=3, timeout=300): """Starts a job, then polls it until it is no longer running.""" start = session.post(f"{API}{path}", json=payload).json() job_id = start.get("jobId") if not job_id: raise RuntimeError(start.get("error", "job not started")) deadline = time.time() + timeout while time.time() < deadline: time.sleep(every) data = session.get(f"{API}{path}/{job_id}").json().get("data") or {} if data.get("status") != "running": return {"jobId": job_id, **data} return {"jobId": job_id, "status": "running"} # keep the jobId, come back later print(run_job("/tools/verify/email", {"email": "claire.fontaine@nexora.io"})) ``` A job still running after your timeout is not lost: keep its `jobId` and read it later. Never start the same search again instead: it would be billed twice. ## Costs and limits | Tool | Cost | Refunded when | | -------------- | ----------- | ------------------------------------------------------------------- | | Email Finder | 1 credit | No email is found, or the search could not start. Not on a timeout. | | Phone Finder | 50 credits | No number is found, or the search could not start. | | Email Verifier | 0.25 credit | Never. | Credits are taken when the job starts. See [Credits](https://docs.emelia.io/docs/credits.md) for the history of your debits. The `/tools/...` routes are not counted in the per minute [rate limit](https://docs.emelia.io/docs/rate-limits.md), and their polling is free. ## Enrich many contacts There is no batch route for the tools: you start one job per contact. To go fast: 1. **Filter first.** Remove the contacts you will not reach before spending credits. 2. **Start every job, then poll them.** Waiting for each job before starting the next one turns minutes into hours. 3. **Keep the `jobId` of every contact,** and write the results back to your list with [Update a contact](https://docs.emelia.io/docs/lists/update-contact.md). 4. **Stop at the first credit error.** The next contacts would fail the same way. To find the emails of a whole LinkedIn extraction, use [Find the emails of a scraper](https://docs.emelia.io/docs/linkedin-scraper.md): 1 credit per email found. ## From an AI assistant The [MCP server](https://docs.emelia.io/docs/ai/mcp.md) wraps the three tools: `find_email`, `find_phone` and `verify_email` start the job and wait for the result for up to 90 seconds, and `get_enrichment_result` reads a job that was still running. Note that `find_email` needs `country`, even if the tool does not mark it as required. --- # Email Finder > Find the professional email address of a person from a full name, a company and a country, with the Emelia Email Finder API. 1 credit, refunded when nothing is found. Source: https://docs.emelia.io/docs/enrichment/email-finder The Email Finder API returns the professional email address of a person from a full name and a company. It costs 1 credit per search, refunded when no email is found. ## Start a search [`POST /tools/find/email`](https://docs.emelia.io/docs/enrichment/email-finder/find-email.md) | Field | Required | Example | | ---------------- | -------- | -------------------------------------------------- | | `fullname` | yes | `Claire Fontaine` | | `companyName` | yes | `Nexora` | | `country` | yes | `FR`: the two letter code of the country | | `companyWebsite` | no | `nexora.io`. Helps a lot when you know the domain. | > **country is required.** A request without `country` is refused with `400 {"error": "\"country\" is required", "code": "INVALID_REQUEST"}`, before any job or credit. If you do not know it, deduce it from the website or the company. **curl** ```bash curl -X POST https://api.emelia.io/tools/find/email \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"fullname": "Claire Fontaine", "companyName": "Nexora", "companyWebsite": "nexora.io", "country": "FR"}' # {"success":true,"jobId":"6650f1a2b839d80011c5f001"} ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/tools/find/email', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ fullname: 'Claire Fontaine', companyName: 'Nexora', companyWebsite: 'nexora.io', country: 'FR' }), }); const { jobId } = await res.json(); ``` **Python** ```python import os import requests res = requests.post( "https://api.emelia.io/tools/find/email", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={"fullname": "Claire Fontaine", "companyName": "Nexora", "companyWebsite": "nexora.io", "country": "FR"}, ) job_id = res.json()["jobId"] ``` ## Read the result [`GET /tools/find/email/{jobId}`](https://docs.emelia.io/docs/enrichment/email-finder/get-result.md) Poll every 3 to 5 seconds until `status` is no longer `running`. The [Enrichment overview](https://docs.emelia.io/docs/enrichment.md#how-a-job-works) has a ready-made polling helper. **curl** ```bash curl https://api.emelia.io/tools/find/email/6650f1a2b839d80011c5f001 \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch(`https://api.emelia.io/tools/find/email/${jobId}`, { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { data } = await res.json(); if (data.status === 'done') console.log(data.email ?? 'not found', data.qualification); ``` **Python** ```python res = requests.get( f"https://api.emelia.io/tools/find/email/{job_id}", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) data = res.json()["data"] if data["status"] == "done": print(data.get("email", "not found"), data.get("qualification")) ``` ```json title="Response" { "success": true, "data": { "_id": "6650f1a2b839d80011c5f001", "fullname": "Claire Fontaine", "companyName": "Nexora", "companyWebsite": "nexora.io", "status": "done", "email": "claire.fontaine@nexora.io", "qualification": "valid", "fallbackFound": false, "date": "2026-10-11T08:00:00.000Z" } } ``` | `status` | Meaning | | --------- | --------------------------------------------------------------------------------------------------- | | `running` | Not finished. Keep polling. | | `done` | Finished. `email` holds the address; no `email` means nothing was found (and the credit came back). | | `error` | The job failed. | `qualification` is the verdict of the provider on the address found, for example `valid`. It is `invalid`, without `email`, when nothing was found or when the search timed out. ## How long it takes Most searches end within seconds. When `companyWebsite` is given and finds nothing, Emelia runs a second pass on the company name alone (`fallbackFound` tells you it found the email there). Each pass can last about 2 minutes, so a search can take up to about 4 minutes. A timed out search keeps its credit. ## Past searches [`GET /tools/find/email`](https://docs.emelia.io/docs/enrichment/email-finder/list-past-searches.md) Your searches, newest first, with `page` from 1 and `limit` (15 by default, 100 at most). Use it to find a result you lost track of rather than paying for the search again. ## Tips * **Send the domain.** `companyWebsite` gives better results than the company name alone. * **Do not verify what the Email Finder found as `valid`:** the [Email Verifier](https://docs.emelia.io/docs/enrichment/email-verifier.md) would bill 0.25 credit for the same answer. * **Polling is free** and not counted in the per minute quota. --- # Find the email of a person Source: https://docs.emelia.io/docs/enrichment/email-finder/find-email Guide: [Email Finder](https://docs.emelia.io/docs/enrichment/email-finder.md) `POST https://api.emelia.io/tools/find/email` Starts an email search for one person and returns a `jobId` immediately. Cost: 1 credit, debited when the job starts and refunded when no email is found (it is NOT refunded when the search times out). The search runs in two passes when `companyWebsite` is given and finds nothing: a second pass with the company name only. Poll `GET /tools/find/email/{jobId}` every 3 to 5 seconds until `status` is `done`; a search can take up to about 2 minutes per pass. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `fullname` | string | yes | First and last name. | | `companyName` | string | yes | | | `companyWebsite` | string | no | Company domain or website. Strongly improves results. | | `country` | string | yes | REQUIRED. Country code of the person, ISO 3166-1 alpha-2 (for example `FR`), passed to the provider as `country_code`. | ```json { "fullname": "John Doe", "companyName": "Acme", "companyWebsite": "acme.com", "country": "FR" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/tools/find/email" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fullname": "John Doe", "companyName": "Acme", "companyWebsite": "acme.com", "country": "FR" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/find/email', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "fullname": "John Doe", "companyName": "Acme", "companyWebsite": "acme.com", "country": "FR" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/tools/find/email", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "fullname": "John Doe", "companyName": "Acme", "companyWebsite": "acme.com", "country": "FR" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `jobId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "jobId": "6650f1a2b839d80011c5f001" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Get an email search result Source: https://docs.emelia.io/docs/enrichment/email-finder/get-result Guide: [Email Finder](https://docs.emelia.io/docs/enrichment/email-finder.md) `GET https://api.emelia.io/tools/find/email/{jobId}` Returns the job. While `status` is `running`, poll again. When `done`, `email` holds the result (absent if nothing was found). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `jobId` | string | yes | The `jobId` returned by the POST route. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/tools/find/email/6650f1a2b839d80011c5f001" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/find/email/6650f1a2b839d80011c5f001', { 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/tools/find/email/6650f1a2b839d80011c5f001", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `data` | object | no | | | `data._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `data.fullname` | string | no | | | `data.companyName` | string | no | | | `data.companyWebsite` | string | no | | | `data.status` | enum: `running`, `done`, `error` | no | | | `data.email` | string | no | Found email, absent when nothing was found. | | `data.qualification` | string | no | Qualification returned by the provider for the found email. `invalid` with no email when nothing was found or after a timeout. | | `data.fallbackFound` | boolean | no | True when the email was found by the second pass (company name only, used when the domain pass found nothing). | | `data.date` | string (date-time) | no | | ```json { "success": true, "data": { "_id": "6650f1a2b839d80011c5f001", "fullname": "John Doe", "companyName": "Acme", "companyWebsite": "acme.com", "status": "done", "email": "john.doe@acme.com", "qualification": "valid", "fallbackFound": false, "date": "2026-10-11T08:00:00.000Z" } } ``` ### 400: Unknown job. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # List email searches Source: https://docs.emelia.io/docs/enrichment/email-finder/list-past-searches Guide: [Email Finder](https://docs.emelia.io/docs/enrichment/email-finder.md) `GET https://api.emelia.io/tools/find/email` History of your single email searches, newest first. `page` is one based. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page` | integer | no | Default: `1`. Range: 1 to .... | | `limit` | integer | no | Default: `15`. Range: 1 to 100. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/tools/find/email" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/find/email', { 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/tools/find/email", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `data` | array of object | no | | | `data[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `data[].fullname` | string | no | | | `data[].companyName` | string | no | | | `data[].companyWebsite` | string | no | | | `data[].status` | enum: `running`, `done`, `error` | no | | | `data[].email` | string | no | Found email, absent when nothing was found. | | `data[].qualification` | string | no | Qualification returned by the provider for the found email. `invalid` with no email when nothing was found or after a timeout. | | `data[].fallbackFound` | boolean | no | True when the email was found by the second pass (company name only, used when the domain pass found nothing). | | `data[].date` | string (date-time) | no | | | `total` | integer | no | | | `page` | integer | no | | | `pageSize` | integer | no | | ```json { "success": true, "data": [ { "_id": "6650f1a2b839d80011c5a0f1", "fullname": "Claire Fontaine", "companyName": "Nexora", "companyWebsite": "nexora.io", "status": "running", "email": "claire.fontaine@nexora.io", "qualification": "valid", "fallbackFound": true, "date": "2026-10-06T09:30:00.000Z" } ], "total": 250, "page": 1, "pageSize": 20 } ``` ### 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" } ``` --- # Phone Finder > Find the phone number of a person from a LinkedIn profile URL with the Emelia Phone Finder API. 50 credits, refunded when no number is found. Source: https://docs.emelia.io/docs/enrichment/phone-finder The Phone Number Finder API returns the phone number of a person from a LinkedIn profile URL. It costs 50 credits per search, refunded when no number is found. ## Start a search [`POST /tools/find/phone`](https://docs.emelia.io/docs/enrichment/phone-finder/find-phone-number.md) The only input is a public LinkedIn profile URL, `https://www.linkedin.com/in/`. A URL without `linkedin.com/in/`, such as a Sales Navigator lead URL, is refused with `Invalid LinkedIn URL`. **curl** ```bash curl -X POST https://api.emelia.io/tools/find/phone \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"linkedinUrl": "https://www.linkedin.com/in/claire-fontaine"}' # {"success":true,"jobId":"6650f1a2b839d80011c5f002"} ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/tools/find/phone', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ linkedinUrl: 'https://www.linkedin.com/in/claire-fontaine' }), }); const { jobId } = await res.json(); ``` **Python** ```python import os import requests res = requests.post( "https://api.emelia.io/tools/find/phone", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={"linkedinUrl": "https://www.linkedin.com/in/claire-fontaine"}, ) job_id = res.json()["jobId"] ``` ## Read the result [`GET /tools/find/phone/{jobId}`](https://docs.emelia.io/docs/enrichment/phone-finder/get-result.md) **curl** ```bash curl https://api.emelia.io/tools/find/phone/6650f1a2b839d80011c5f002 \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch(`https://api.emelia.io/tools/find/phone/${jobId}`, { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { data } = await res.json(); if (data.qualification === 'found') console.log(data.phoneNumber, data.country); ``` **Python** ```python res = requests.get( f"https://api.emelia.io/tools/find/phone/{job_id}", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) data = res.json()["data"] if data.get("qualification") == "found": print(data["phoneNumber"], data["country"]) ``` ```json title="Response" { "success": true, "data": { "_id": "6650f1a2b839d80011c5f002", "linkedinUrl": "https://www.linkedin.com/in/claire-fontaine", "status": "done", "qualification": "found", "phoneNumber": "+33612345678", "country": "FR", "date": "2026-10-11T08:00:00.000Z" } } ``` `qualification` is `found` and `phoneNumber` is set when a number was found. Otherwise the 50 credits come back. The result arrives from the provider when it is ready: there is no fixed upper bound on the wait. Poll every few seconds, then less often, and keep the `jobId` if you stop. A number already found for the same profile in the last 6 months is returned at once, and costs 50 credits too. ## Past searches [`GET /tools/find/phone`](https://docs.emelia.io/docs/enrichment/phone-finder/list-past-searches.md) Your searches, newest first, with `page` from 1 and `limit` (15 by default, 100 at most). Check it before searching a profile again. ## Tips * **Search only the contacts you will call.** At 50 credits a search, filter first. * **Store the number in `phone`** with [Update a contact](https://docs.emelia.io/docs/lists/update-contact.md): it is the field lists count. * **Polling is free** and not counted in the per minute quota. --- # Find the phone number of a person Source: https://docs.emelia.io/docs/enrichment/phone-finder/find-phone-number Guide: [Phone Finder](https://docs.emelia.io/docs/enrichment/phone-finder.md) `POST https://api.emelia.io/tools/find/phone` Starts a mobile phone search from a LinkedIn profile URL and returns a `jobId`. Cost: 50 credits, debited when the job starts and refunded when no number is found (results found during the last 6 months are served from cache and still cost 50 credits). The result arrives asynchronously from the provider: poll `GET /tools/find/phone/{jobId}` until `status` is `done`; there is no fixed upper bound on the wait. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `linkedinUrl` | string | yes | Must contain `linkedin.com/in/`. | ```json { "linkedinUrl": "https://www.linkedin.com/in/johndoe" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/tools/find/phone" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "linkedinUrl": "https://www.linkedin.com/in/johndoe" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/find/phone', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "linkedinUrl": "https://www.linkedin.com/in/johndoe" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/tools/find/phone", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "linkedinUrl": "https://www.linkedin.com/in/johndoe" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `jobId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "jobId": "6650f1a2b839d80011c5f002" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Get a phone search result Source: https://docs.emelia.io/docs/enrichment/phone-finder/get-result Guide: [Phone Finder](https://docs.emelia.io/docs/enrichment/phone-finder.md) `GET https://api.emelia.io/tools/find/phone/{jobId}` Returns the job. `qualification` is `found` and `phoneNumber` is set when a number was found. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `jobId` | string | yes | The `jobId` returned by the POST route. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/tools/find/phone/6650f1a2b839d80011c5f001" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/find/phone/6650f1a2b839d80011c5f001', { 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/tools/find/phone/6650f1a2b839d80011c5f001", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `data` | object | no | | | `data._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `data.linkedinUrl` | string | no | | | `data.status` | enum: `running`, `done`, `error` | no | | | `data.qualification` | string | no | `found` when a number was found. | | `data.phoneNumber` | string | no | | | `data.country` | string | no | | | `data.date` | string (date-time) | no | | ```json { "success": true, "data": { "_id": "6650f1a2b839d80011c5f002", "linkedinUrl": "https://www.linkedin.com/in/johndoe", "status": "done", "qualification": "found", "phoneNumber": "+33612345678", "country": "FR", "date": "2026-10-11T08:00:00.000Z" } } ``` ### 400: Unknown job. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # List phone searches Source: https://docs.emelia.io/docs/enrichment/phone-finder/list-past-searches Guide: [Phone Finder](https://docs.emelia.io/docs/enrichment/phone-finder.md) `GET https://api.emelia.io/tools/find/phone` History of your single phone searches, newest first. `page` is one based. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page` | integer | no | Default: `1`. Range: 1 to .... | | `limit` | integer | no | Default: `15`. Range: 1 to 100. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/tools/find/phone" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/find/phone', { 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/tools/find/phone", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `data` | array of object | no | | | `data[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `data[].linkedinUrl` | string | no | | | `data[].status` | enum: `running`, `done`, `error` | no | | | `data[].qualification` | string | no | `found` when a number was found. | | `data[].phoneNumber` | string | no | | | `data[].country` | string | no | | | `data[].date` | string (date-time) | no | | | `total` | integer | no | | | `page` | integer | no | | | `pageSize` | integer | no | | ```json { "success": true, "data": [ { "_id": "6650f1a2b839d80011c5a0f1", "linkedinUrl": "https://www.linkedin.com/in/claire-fontaine", "status": "running", "qualification": "found", "phoneNumber": "+33 6 12 34 56 78", "country": "FR", "date": "2026-10-06T09:30:00.000Z" } ], "total": 250, "page": 1, "pageSize": 20 } ``` ### 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" } ``` --- # Email Verifier > Verify that an email address can receive mail before you send, with the Emelia Email Verification API. 0.25 credit per verification. Source: https://docs.emelia.io/docs/enrichment/email-verifier The Email Verification API checks whether an address can receive mail. It costs 0.25 credit per verification, whatever the result: the credit is never refunded. ## Start a verification [`POST /tools/verify/email`](https://docs.emelia.io/docs/enrichment/email-verifier/verify-email.md) **curl** ```bash curl -X POST https://api.emelia.io/tools/verify/email \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "claire.fontaine@nexora.io"}' # {"success":true,"jobId":"6650f1a2b839d80011c5f003"} ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/tools/verify/email', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'claire.fontaine@nexora.io' }), }); const { jobId } = await res.json(); ``` **Python** ```python import os import requests res = requests.post( "https://api.emelia.io/tools/verify/email", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={"email": "claire.fontaine@nexora.io"}, ) job_id = res.json()["jobId"] ``` ## Read the result [`GET /tools/verify/email/{jobId}`](https://docs.emelia.io/docs/enrichment/email-verifier/get-result.md) **curl** ```bash curl https://api.emelia.io/tools/verify/email/6650f1a2b839d80011c5f003 \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch(`https://api.emelia.io/tools/verify/email/${jobId}`, { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { data } = await res.json(); if (data.status === 'done') console.log(data.email, data.qualification); ``` **Python** ```python res = requests.get( f"https://api.emelia.io/tools/verify/email/{job_id}", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) data = res.json()["data"] if data["status"] == "done": print(data["email"], data["qualification"]) ``` ```json title="Response" { "success": true, "data": { "_id": "6650f1a2b839d80011c5f003", "email": "claire.fontaine@nexora.io", "status": "done", "qualification": "valid", "date": "2026-10-11T08:00:00.000Z" } } ``` `qualification` is the deliverability verdict of the provider, for example `valid`. A verification usually ends within seconds; after about 60 seconds without a verdict the job ends with `status: "error"`, and the 0.25 credit is not refunded. ## Past verifications [`GET /tools/verify/email`](https://docs.emelia.io/docs/enrichment/email-verifier/list-past-verifications.md) Your verifications, newest first, with `page` from 1 and `limit` (15 by default, 100 at most). ## When to verify * **Verify the addresses you bring yourself:** an old CRM export, a file from a partner, a form. * **Do not verify what the [Email Finder](https://docs.emelia.io/docs/enrichment/email-finder.md) returned as `valid`:** it was already checked. * **Catch-all domains** accept every address, real or not: no verifier can prove that a specific mailbox exists behind them. Send to those addresses in small batches and watch the bounces. --- # Verify an email address Source: https://docs.emelia.io/docs/enrichment/email-verifier/verify-email Guide: [Email Verifier](https://docs.emelia.io/docs/enrichment/email-verifier.md) `POST https://api.emelia.io/tools/verify/email` Starts the verification of one email address and returns a `jobId`. Cost: 0.25 credit, never refunded (even when the provider fails). Poll `GET /tools/verify/email/{jobId}` every few seconds; the job is marked `error` after about 60 seconds without a verdict. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `email` | string (email) | yes | | ```json { "email": "john.doe@acme.com" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/tools/verify/email" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "john.doe@acme.com" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/verify/email', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "email": "john.doe@acme.com" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/tools/verify/email", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "email": "john.doe@acme.com" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `jobId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "jobId": "6650f1a2b839d80011c5f003" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Get an email verification result Source: https://docs.emelia.io/docs/enrichment/email-verifier/get-result Guide: [Email Verifier](https://docs.emelia.io/docs/enrichment/email-verifier.md) `GET https://api.emelia.io/tools/verify/email/{jobId}` Returns the job with its `qualification` once `status` is `done`. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `jobId` | string | yes | The `jobId` returned by the POST route. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/tools/verify/email/6650f1a2b839d80011c5f001" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/verify/email/6650f1a2b839d80011c5f001', { 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/tools/verify/email/6650f1a2b839d80011c5f001", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `data` | object | no | | | `data._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `data.email` | string | no | | | `data.status` | enum: `running`, `done`, `error` | no | | | `data.qualification` | string | no | Deliverability verdict returned by the provider. | | `data.date` | string (date-time) | no | | ```json { "success": true, "data": { "_id": "6650f1a2b839d80011c5f003", "email": "john.doe@acme.com", "status": "done", "qualification": "valid", "date": "2026-10-11T08:00:00.000Z" } } ``` ### 400: Unknown job. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # List email verifications Source: https://docs.emelia.io/docs/enrichment/email-verifier/list-past-verifications Guide: [Email Verifier](https://docs.emelia.io/docs/enrichment/email-verifier.md) `GET https://api.emelia.io/tools/verify/email` History of your single verifications, newest first. `page` is one based. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page` | integer | no | Default: `1`. Range: 1 to .... | | `limit` | integer | no | Default: `15`. Range: 1 to 100. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/tools/verify/email" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/tools/verify/email', { 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/tools/verify/email", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `data` | array of object | no | | | `data[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `data[].email` | string | no | | | `data[].status` | enum: `running`, `done`, `error` | no | | | `data[].qualification` | string | no | Deliverability verdict returned by the provider. | | `data[].date` | string (date-time) | no | | | `total` | integer | no | | | `page` | integer | no | | | `pageSize` | integer | no | | ```json { "success": true, "data": [ { "_id": "6650f1a2b839d80011c5a0f1", "email": "claire.fontaine@nexora.io", "status": "running", "qualification": "valid", "date": "2026-10-06T09:30:00.000Z" } ], "total": 250, "page": 1, "pageSize": 20 } ``` ### 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" } ``` --- # Replies > Get the replies of your Emelia campaigns as they come, read them in batches or as a CSV, see what was sent, and answer from the right mailbox. Source: https://docs.emelia.io/docs/replies Replies reach you in several ways. Pick by need: | Need | Use | | ------------------------------------------------------- | --------------------------------------------------------------------------------------- | | React the moment someone answers (CRM, Slack, routing) | [Webhooks](https://docs.emelia.io/docs/webhooks.md) on `MAIL_REPLIED`, `LINKEDIN_REPLIED` and `WHATSAPP_REPLIED` | | Process replies in batches, every hour or every morning | The activity feed of each campaign, below | | Every reply of a campaign in one file | [Export as CSV](https://docs.emelia.io/docs/campaigns/export-as-csv.md) with `type=replies` | The Python examples use one session: ```python title="Python setup" import os import requests session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] ``` ## Read the replies of a campaign [`GET /advanced/campaigns/{campaignId}/activities`](https://docs.emelia.io/docs/replies/list-campaign-activities.md) The feed returns 30 activities per page, newest first, with `page` starting at `0`. An empty `activities` array means you reached the end. Pass `types=MAIL_REPLIED,LINKEDIN_REPLIED,WHATSAPP_REPLIED` (comma separated) to get the replies of every channel in one pass. **curl** ```bash curl "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities?types=MAIL_REPLIED,LINKEDIN_REPLIED&page=0" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js async function replies(campaignId) { const all = []; for (let page = 0; ; page++) { const url = `https://api.emelia.io/advanced/campaigns/${campaignId}/activities?types=MAIL_REPLIED,LINKEDIN_REPLIED&page=${page}`; const res = await fetch(url, { headers: { Authorization: process.env.EMELIA_API_KEY } }); const { activities } = await res.json(); if (!activities?.length) return all; all.push(...activities); } } ``` **Python** ```python def replies(campaign_id): found, page = [], 0 while True: res = session.get( f"https://api.emelia.io/advanced/campaigns/{campaign_id}/activities", params={"types": "MAIL_REPLIED,LINKEDIN_REPLIED", "page": page}, ).json() if not res.get("activities"): return found found += res["activities"] page += 1 ``` A reply looks like this: ```json { "_id": "6650f1a2b839d80011c5e001", "event": "MAIL_REPLIED", "date": "2026-10-08T09:12:00.000Z", "step": 2, "version": 0, "contact": { "_id": "6650f1a2b839d80011c5d001", "email": "claire.fontaine@nexora.io", "firstName": "Claire", "lastName": "Fontaine" }, "customData": {}, "reply": { "subject": "Re: Quick question about Nexora", "text": "Sounds good, call me Tuesday.", "content": "

Sounds good, call me Tuesday.

", "senderName": "Claire Fontaine" } } ``` * `reply.text` is the plain text, `reply.content` the HTML. * Filter one contact with `contactId`, or search by name, email or LinkedIn URL with `search`. * The feed is cached for up to 10 minutes: a reply that just arrived can take a few minutes to show here. [Webhooks](https://docs.emelia.io/docs/webhooks.md) are immediate. ## See what was sent To show a reply next to the message that triggered it, rebuild the content of a send activity (`SENT`, `MESSAGE_SENT`, `INMAIL_SENT`, `INVITED` or `WHATSAPP_MESSAGE_SENT`): [`GET /advanced/campaigns/{campaignId}/activities/{activityId}/message`](https://docs.emelia.io/docs/replies/get-sent-message.md) **curl** ```bash curl https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities/6650f1a2b839d80011c5e002/message \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch( 'https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities/6650f1a2b839d80011c5e002/message', { headers: { Authorization: process.env.EMELIA_API_KEY } }, ); const { channel, subject, message } = await res.json(); ``` **Python** ```python res = session.get( "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities/6650f1a2b839d80011c5e002/message" ) print(res.json()) # {"channel": "EMAIL", "subject": "...", "message": "

...

", "attachments": []} ``` The content is not stored: it is rebuilt from the step and the contact, so it can differ if either changed since. This route answers `404 {"error": "NOT_FOUND"}` or `422 {"error": "NO_MESSAGE"}` instead of the usual format. ## Answer an email reply [`POST /emails/reply`](https://docs.emelia.io/docs/replies/send-reply.md) This route sends an email from one of your connected mailboxes, whatever the campaign. It comes from the previous system and is marked deprecated in the reference, but it is the route to answer a reply by API today. | Field | Description | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `content` | Required. The HTML of your answer. Use `
` or `

` for line breaks. | | `providerId` or `senderEmail` | Required, one of the two: the mailbox that sends. `providerId` is the `_id` from [List email providers](https://docs.emelia.io/docs/email-providers/list-email-providers.md). | | `messageId` | The Message-ID of the email you answer. With it, your answer is threaded, with the quoted history. If Emelia cannot find it, the call fails with "Original message not found". | | `to`, `subject` | Required without `messageId`. | | `cc`, `bcc`, `attachments` | Optional. `attachments` is a list of `{ "name", "url" }`. | | `scheduledAt` | Optional ISO date: schedules the send instead of sending now. | **curl** ```bash curl -X POST https://api.emelia.io/emails/reply \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "senderEmail": "marie.martin@yourcompany.com", "to": ["claire.fontaine@nexora.io"], "subject": "Re: Quick question about Nexora", "content": "Hi Claire,

Tuesday 10am works. I just sent the invite.

Marie" }' ``` **JavaScript** ```js await fetch('https://api.emelia.io/emails/reply', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ senderEmail: 'marie.martin@yourcompany.com', to: ['claire.fontaine@nexora.io'], subject: 'Re: Quick question about Nexora', content: 'Hi Claire,

Tuesday 10am works. I just sent the invite.

Marie', }), }); ``` **Python** ```python session.post("https://api.emelia.io/emails/reply", json={ "senderEmail": "marie.martin@yourcompany.com", "to": ["claire.fontaine@nexora.io"], "subject": "Re: Quick question about Nexora", "content": "Hi Claire,

Tuesday 10am works. I just sent the invite.

Marie", }) ``` Answer from the mailbox that received the reply. The `MAIL_REPLIED` [webhook](https://docs.emelia.io/docs/webhooks.md) gives it (`sender`) along with the `messageId` of the reply. ## Stop a sequence after a reply By default, a reply by email, LinkedIn or WhatsApp ends the sequence for that contact (`eventToStop` in the [settings](https://docs.emelia.io/docs/campaigns.md#3-set-the-schedule)). Someone who asks not to be contacted again belongs in the [blacklist](https://docs.emelia.io/docs/blacklist.md): it protects all your future campaigns too. --- # List campaign activities Source: https://docs.emelia.io/docs/replies/list-campaign-activities Guide: [Replies](https://docs.emelia.io/docs/replies.md) `GET https://api.emelia.io/advanced/campaigns/{campaignId}/activities` Activities of the campaign, newest first, 30 per page. `page` is ZERO based; an empty array means you reached the end. Results are cached for up to 10 minutes. Filters: `contactId` wins over `search`, which wins over `query`. When `query` is an exact email, the type filters are ignored. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page` | string | no | Default: `"0"`. | | `type` | enum: `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `LINKEDIN_REPLIED`, `MAIL_REPLIED`, `RE_REPLY_EMAIL`, `RE_REPLY_LINKEDIN`, `FOLLOWED`, `LIKED`, `SENT`, `BOUNCED`, `RE_REPLY`, `OPENED`, `UNSUBSCRIBED`, `CLICKED`, `WHATSAPP_MESSAGE_SENT`, `WHATSAPP_REPLIED`, `TASK_COMPLETED`, `TASK_FAILED`, `FAILED_ACTION`, `EXCLUDED` | no | Single event filter. `MESSAGE_SENT` also returns `INMAIL_SENT`. | | `types` | string | no | Comma separated events, for example `MAIL_REPLIED,LINKEDIN_REPLIED`. Wins over `type`. | | `contactId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `versionId` | string | no | | | `search` | string | no | Search contacts by name, email or LinkedIn URL. | | `query` | string | no | Legacy filter: an exact email address. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities', { 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/campaigns/6650f1a2b839d80011c5a0f1/activities", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `activities` | array of object | no | | | `activities[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `activities[].contact` | object | no | The full contact document (all stored fields except `__v`). | | `activities[].event` | enum: `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `LINKEDIN_REPLIED`, `MAIL_REPLIED`, `RE_REPLY_EMAIL`, `RE_REPLY_LINKEDIN`, `FOLLOWED`, `LIKED`, `SENT`, `BOUNCED`, `RE_REPLY`, `OPENED`, `UNSUBSCRIBED`, `CLICKED`, `WHATSAPP_MESSAGE_SENT`, `WHATSAPP_REPLIED`, `TASK_COMPLETED`, `TASK_FAILED`, `FAILED_ACTION`, `EXCLUDED` | no | Activity event of a multichannel campaign. `SENT`, `OPENED`, `CLICKED`, `BOUNCED`, `UNSUBSCRIBED`, `MAIL_REPLIED` are email events; `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `LIKED`, `FOLLOWED`, `LINKEDIN_REPLIED` are LinkedIn events. `RE_REPLY` and `FAILED_ACTION` are never returned by the activities route. | | `activities[].date` | string (date-time) | no | | | `activities[].customData` | object | no | Event specific data (for example the clicked link), internal keys removed. | | `activities[].step` | integer | no | Depth of the step in the sequence tree (1 = first step after START). | | `activities[].version` | integer | no | Index of the A/B version (0 = A) when the step has several versions, otherwise -1. | | `activities[].reply` | object | no | Only for `MAIL_REPLIED` and `LINKEDIN_REPLIED`. | | `activities[].sender` | object | no | Account that performed the action, when known. | ```json { "activities": [ { "_id": "6650f1a2b839d80011c5e001", "contact": { "_id": "6650f1a2b839d80011c5d001", "email": "john.doe@acme.com", "firstName": "John", "lastName": "Doe" }, "event": "MAIL_REPLIED", "date": "2026-10-08T09:12:00.000Z", "customData": {}, "step": 2, "version": 0, "reply": { "content": "

Sounds good, call me Tuesday.

", "text": "Sounds good, call me Tuesday.", "subject": "Re: Quick question, John", "senderName": "John Doe" } } ] } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Get the message of a send activity Source: https://docs.emelia.io/docs/replies/get-sent-message Guide: [Replies](https://docs.emelia.io/docs/replies.md) `GET https://api.emelia.io/advanced/campaigns/{campaignId}/activities/{activityId}/message` Rebuilds the content sent for a `SENT`, `MESSAGE_SENT`, `INMAIL_SENT`, `INVITED` or `WHATSAPP_MESSAGE_SENT` activity by replaying the step template against the contact. The sent content itself is not stored: the result can differ if the step or the contact changed since. Errors use a different format here: `{"error": "NOT_FOUND"}` (404) or `{"error": "NO_MESSAGE"}` (422). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. | | `activityId` | string | yes | Activity id. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities/6650f1a2b839d80011c5e001/message" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/activities/6650f1a2b839d80011c5e001/message', { 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/campaigns/6650f1a2b839d80011c5a0f1/activities/6650f1a2b839d80011c5e001/message", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `channel` | enum: `EMAIL`, `LINKEDIN`, `WHATSAPP` | no | | | `subject` | string or null | no | | | `message` | string | no | | | `attachments` | array of object | no | | | `attachments[].name` | string | no | | | `attachments[].mediaType` | string | no | | ```json { "channel": "EMAIL", "subject": "Quick question, John", "message": "

Hi John,

", "attachments": [] } ``` ### 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" } ``` ### 404: Unknown activity, campaign, step or contact. | 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": "NOT_FOUND" } ``` ### 422: The activity is not a send event. | 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": "NO_MESSAGE" } ``` ### 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" } ``` --- # Send a reply from a connected mailbox Source: https://docs.emelia.io/docs/replies/send-reply Guide: [Replies](https://docs.emelia.io/docs/replies.md) `POST https://api.emelia.io/emails/reply` Sends an e-mail from one of your connected providers. At least one of providerId or senderEmail is required to pick the mailbox. With messageId (from the merged inbox) the message is threaded (In-Reply-To, References, quoted history appended); without it you must supply to and subject yourself. content is HTML (use
for line breaks). scheduledAt (ISO) schedules the send instead of sending now. New integrations should use the multichannel /advanced campaign endpoints instead. > **Deprecated.** ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `senderEmail` | string (email) | no | Sender mailbox address (alternative to providerId). | | `providerId` | string | no | Provider ObjectId (alternative to senderEmail). | | `messageId` | string | no | Message-ID of the email to reply to. Threads the reply. If Emelia cannot find it, the call fails with 'Original message not found'. | | `subject` | string or null | no | | | `content` | string | yes | HTML body. Use
for line breaks. | | `scheduledAt` | string (date-time) | no | ISO date to schedule the send. | | `attachments` | array of object | no | | | `attachments[].name` | string | no | | | `attachments[].url` | string (uri) | no | | | `to` | array of string | no | | | `cc` | array of string | no | | | `bcc` | array of string | no | | ```json { "providerId": "5f3a1b2c4d5e6f0011223344", "to": [ "jane.roe@acme.com" ], "subject": "Re: your question", "content": "Hello Jane,

Thanks for your reply." } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/reply" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "providerId": "5f3a1b2c4d5e6f0011223344", "to": [ "jane.roe@acme.com" ], "subject": "Re: your question", "content": "Hello Jane,

Thanks for your reply." }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/reply', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "providerId": "5f3a1b2c4d5e6f0011223344", "to": [ "jane.roe@acme.com" ], "subject": "Re: your question", "content": "Hello Jane,

Thanks for your reply." }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/reply", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "providerId": "5f3a1b2c4d5e6f0011223344", "to": [ "jane.roe@acme.com" ], "subject": "Re: your question", "content": "Hello Jane,

Thanks for your reply." }, ) print(response.json()) ``` ## Responses ### 200: Reply sent (or scheduled). | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Blacklist > Add an email address, a whole domain or a LinkedIn profile to the Emelia blacklist by API, so that no campaign of your account ever contacts them. Source: https://docs.emelia.io/docs/blacklist The blacklist applies to your whole account: a blacklisted address is skipped by every campaign, current and future, before each email and each LinkedIn action. Use it for people who ask not to be contacted again, competitors and your customers. The routes start with `/emails/` for historical reasons, but they apply to every campaign, multichannel and legacy. ## Add an address or a domain [`POST /emails/blacklists/contact`](https://docs.emelia.io/docs/blacklist/blacklist-email-or-domain.md) **curl** ```bash curl -X POST https://api.emelia.io/emails/blacklists/contact \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "claire.fontaine@nexora.io"}' ``` **JavaScript** ```js await fetch('https://api.emelia.io/emails/blacklists/contact', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'claire.fontaine@nexora.io' }), }); ``` **Python** ```python import os import requests requests.post( "https://api.emelia.io/emails/blacklists/contact", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={"email": "claire.fontaine@nexora.io"}, ) ``` The `email` field also takes a bare domain, `nexora.io`, which blacklists everyone at that company. Think twice before doing it: it is account wide. To remove an entry, send the same body to [Remove an email or domain](https://docs.emelia.io/docs/blacklist/remove-email-or-domain.md): [`DELETE /emails/blacklists/contact`](https://docs.emelia.io/docs/blacklist/remove-email-or-domain.md) ## LinkedIn profiles [`POST /emails/blacklists/linkedin`](https://docs.emelia.io/docs/blacklist/blacklist-linkedin-profile.md) **curl** ```bash curl -X POST https://api.emelia.io/emails/blacklists/linkedin \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"value": "https://www.linkedin.com/in/claire-fontaine"}' ``` **JavaScript** ```js await fetch('https://api.emelia.io/emails/blacklists/linkedin', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ value: 'https://www.linkedin.com/in/claire-fontaine' }), }); ``` **Python** ```python requests.post( "https://api.emelia.io/emails/blacklists/linkedin", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={"value": "https://www.linkedin.com/in/claire-fontaine"}, ) ``` `value` is a full profile URL or a bare handle (`claire-fontaine`): it is stored as the handle. [Remove a LinkedIn profile](https://docs.emelia.io/docs/blacklist/remove-linkedin-profile.md) takes the same body. ## Read the blacklists [`GET /emails/blacklists`](https://docs.emelia.io/docs/blacklist/get-blacklists.md) **curl** ```bash curl https://api.emelia.io/emails/blacklists \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/emails/blacklists', { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const [blacklist] = await res.json(); // a plain array const emails = blacklist?.blacklistContent?.split('\n').filter(Boolean) ?? []; const linkedin = blacklist?.linkedinBlacklistContent?.split('\n').filter(Boolean) ?? []; ``` **Python** ```python res = requests.get( "https://api.emelia.io/emails/blacklists", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) items = res.json() # a plain array emails = items[0]["blacklistContent"].split("\n") if items else [] ``` The response is a plain array, not wrapped in `success`. Each item holds `blacklistContent` (addresses and domains) and `linkedinBlacklistContent` (LinkedIn handles), one entry per line. ## Automatic blacklisting * With `blacklistUnsub: true` in the [campaign settings](https://docs.emelia.io/docs/campaigns/update-campaign-settings.md), a contact who unsubscribes is blacklisted on its own. * A contact who **writes** "please remove me" in a reply is not caught by that setting. Read the [replies](https://docs.emelia.io/docs/replies.md) and blacklist those contacts yourself, for example from your webhook handler. --- # Add an email or domain to the account blacklist Source: https://docs.emelia.io/docs/blacklist/blacklist-email-or-domain Guide: [Blacklist](https://docs.emelia.io/docs/blacklist.md) `POST https://api.emelia.io/emails/blacklists/contact` Adds an address, or a bare domain (domain.ltd), to the workspace blacklist. The blacklist is account-wide and applies to every current and future campaign. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `email` | string | yes | Email address or bare domain (e.g. acme.com). | ```json { "email": "john.doe@acme.com" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/blacklists/contact" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "john.doe@acme.com" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/blacklists/contact', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "email": "john.doe@acme.com" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/blacklists/contact", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "email": "john.doe@acme.com" }, ) print(response.json()) ``` ## Responses ### 200: Added to blacklist. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Remove an email or domain from the account blacklist Source: https://docs.emelia.io/docs/blacklist/remove-email-or-domain Guide: [Blacklist](https://docs.emelia.io/docs/blacklist.md) `DELETE https://api.emelia.io/emails/blacklists/contact` Removes an address (or bare domain) from the workspace blacklist. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `email` | string | yes | | ```json { "email": "john.doe@acme.com" } ``` ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/emails/blacklists/contact" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "john.doe@acme.com" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/blacklists/contact', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "email": "john.doe@acme.com" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/emails/blacklists/contact", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "email": "john.doe@acme.com" }, ) print(response.json()) ``` ## Responses ### 200: Removed from blacklist. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Get the account blacklists Source: https://docs.emelia.io/docs/blacklist/get-blacklists Guide: [Blacklist](https://docs.emelia.io/docs/blacklist.md) `GET https://api.emelia.io/emails/blacklists` Returns the workspace blacklist documents. WARNING: the response is a RAW ARRAY (not wrapped in { success }). Each item carries blacklistContent (email/domain blacklist, newline-separated) and linkedinBlacklistContent (LinkedIn handles, newline-separated). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/emails/blacklists" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/blacklists', { 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/emails/blacklists", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Array of blacklist documents. ```json [ { "_id": "5f3a1b2c4d5e6f00112233cc", "name": "blacklist", "blacklistContent": "john.doe@acme.com\nacme.com", "linkedinBlacklistContent": "john-doe" } ] ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Add a LinkedIn profile to the blacklist Source: https://docs.emelia.io/docs/blacklist/blacklist-linkedin-profile Guide: [Blacklist](https://docs.emelia.io/docs/blacklist.md) `POST https://api.emelia.io/emails/blacklists/linkedin` Adds a LinkedIn profile to the workspace blacklist. value is a full profile URL (https://www.linkedin.com/in/...) or a bare handle; it is normalised to the handle. Despite living under /emails, this manages the LinkedIn blacklist. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `value` | string | yes | LinkedIn profile URL or bare handle. | ```json { "value": "https://www.linkedin.com/in/john-doe/" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/emails/blacklists/linkedin" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "value": "https://www.linkedin.com/in/john-doe/" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/blacklists/linkedin', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "value": "https://www.linkedin.com/in/john-doe/" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/emails/blacklists/linkedin", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "value": "https://www.linkedin.com/in/john-doe/" }, ) print(response.json()) ``` ## Responses ### 200: Added. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Remove a LinkedIn profile from the blacklist Source: https://docs.emelia.io/docs/blacklist/remove-linkedin-profile Guide: [Blacklist](https://docs.emelia.io/docs/blacklist.md) `DELETE https://api.emelia.io/emails/blacklists/linkedin` Removes a LinkedIn profile (URL or handle) from the workspace blacklist. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `value` | string | yes | | ```json { "value": "john-doe" } ``` ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/emails/blacklists/linkedin" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "value": "john-doe" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/emails/blacklists/linkedin', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "value": "john-doe" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/emails/blacklists/linkedin", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "value": "john-doe" }, ) print(response.json()) ``` ## Responses ### 200: Removed. | Field | Type | Required | Description | |---|---|---|---| | `success` | enum: `true` | yes | | ```json { "success": true } ``` ### 400: Business or validation error. 400 by default; Joi validation errors carry code INVALID_REQUEST. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Webhooks > Receive a JSON POST each time something happens in your Emelia campaigns: email sent, opened, clicked, replied, bounced, LinkedIn invitation accepted, WhatsApp reply, campaign finished. Source: https://docs.emelia.io/docs/webhooks A webhook is a URL that Emelia calls with a JSON `POST` each time a selected event happens in a campaign: a reply, an acceptance, a bounce. Use it to update your CRM, alert your team or route replies the moment they arrive. ## Register a webhook Webhooks are registered on a campaign, or on all your campaigns at once. Today they are created from the **Make** integration of Emelia (the "Watch Campaign Activities" and "Watch Campaign Ending" modules), and listed, tested and deleted in the Emelia app, in **Settings, API & Webhooks**. > **Creating webhooks by API.** The registration routes are not part of this reference yet. If you need to register webhooks from your own code, contact [contact@emelia.io](mailto:contact@emelia.io). ## List your webhooks [`GET /webhook`](https://docs.emelia.io/docs/webhooks/list-webhooks.md) **curl** ```bash curl https://api.emelia.io/webhook \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/webhook', { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { webhooks } = await res.json(); for (const hook of webhooks) console.log(hook.campaignName, hook.url, hook.events.join(',')); ``` **Python** ```python import os import requests res = requests.get( "https://api.emelia.io/webhook", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) for hook in res.json()["webhooks"]: print(hook["campaignName"], hook["url"], hook["events"]) ``` ```json title="Response" { "webhooks": [ { "id": "6702c1d4e5f6a7b8c9d0e1f2", "typeOfCampaign": "advanced", "campaignId": "6651f0c2a1b2c3d4e5f60718", "campaignName": "Q4 SaaS founders", "url": "https://hooks.yourcompany.com/emelia", "events": ["MAIL_REPLIED", "LINKEDIN_REPLIED"], "status": "active" } ] } ``` There is one entry per campaign: a URL registered on all your campaigns appears once for each of them. `typeOfCampaign` is `advanced` for the campaigns of the current app, `email` or `linkedin` for [legacy campaigns](https://docs.emelia.io/docs/campaigns/legacy.md). ## Events of a campaign These are the event names of the campaigns of the current app. The payload of each is on its page: [Campaign activity](https://docs.emelia.io/docs/webhooks/campaign-activity.md) and [Campaign finished](https://docs.emelia.io/docs/webhooks/campaign-finished.md). | Event | When | | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `EMAIL_SENT` | An email step was sent. The name is `EMAIL_SENT`, not `MAIL_SENT`. | | `OPENED`, `CLICKED` | The tracking pixel loaded, or a tracked link was clicked. Needs tracking on the campaign. Delivered 30 to 60 seconds later. | | `MAIL_REPLIED` | First email reply of the contact, with `reply`, `sentiment`, `sender` and `messageId`. | | `BOUNCED` | A bounce was detected. No `campaign` field, `step` is 0. | | `UNSUBSCRIBED` | The contact unsubscribed. | | `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `LIKED` | LinkedIn actions. `MESSAGE_SENT` carries the text in `content`. | | `LINKEDIN_REPLIED` | LinkedIn reply, with `reply` and `sentiment`. | | `WHATSAPP_MESSAGE_SENT`, `WHATSAPP_REPLIED` | WhatsApp message sent, reply received. | | `CAMPAIGN_ENDING` | The campaign was closed by the daily job, see [Campaign finished](https://docs.emelia.io/docs/webhooks/campaign-finished.md). | Never sent for these campaigns: `SENT`, `MAIL_SENT`, `FOLLOWED`, `TASK_COMPLETED`, `TASK_FAILED`. The events of legacy campaigns have their own pages: [Legacy email activity](https://docs.emelia.io/docs/webhooks/legacy-email-activity.md), [Legacy LinkedIn activity](https://docs.emelia.io/docs/webhooks/legacy-linkedin-activity.md). A `MAIL_REPLIED` delivery looks like this: ```json { "event": "MAIL_REPLIED", "campaign": "Q4 SaaS founders", "campaignId": "6651f0c2a1b2c3d4e5f60718", "senderName": "Marie Martin", "sender": "marie.martin@yourcompany.com", "contact": { "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "Claire", "lastName": "Fontaine", "email": "claire.fontaine@nexora.io", "company": "Nexora" }, "date": "2026-10-13T10:20:00.000Z", "step": 0, "reply": "Hi Marie, sounds interesting. Can we talk on Tuesday?", "sentiment": { "classification": "INTERESTED", "score": 0.92 }, "messageId": "" } ``` * `contact` starts with the custom variables of the contact, then the standard fields. `company` is the first custom variable whose name contains "company". * `sentiment` is the AI classification of the reply: `INTERESTED`, `NOT_INTERESTED`, `NEUTRAL`, `OUT_OF_OFFICE`, `HOSTILE`, `DELEGATION`, `INCOMPREHENSION` or `QUESTION_ASK`, with a `score`. It is present only when reply classification runs on your account. * `date` is an ISO 8601 date in UTC, usually the time Emelia processed the event. * Absent fields are omitted, never `null`. ## Receive the events Answer fast with a `2xx`, then do the work in the background. **curl** ```bash # Simulate a delivery to test your endpoint curl -X POST https://hooks.yourcompany.com/emelia \ -H "Content-Type: application/json" \ -d '{"event": "MAIL_REPLIED", "campaign": "Q4 SaaS founders", "contact": {"email": "claire.fontaine@nexora.io"}, "date": "2026-10-13T10:20:00.000Z", "step": 0, "reply": "Can we talk on Tuesday?"}' ``` **JavaScript** ```js import express from 'express'; const app = express(); app.use(express.json()); // Keep the path hard to guess: deliveries are not signed. app.post('/emelia/9f2c4e1a7b', (req, res) => { res.sendStatus(200); // answer first const { event, contact, reply, sentiment } = req.body; if (event === 'MAIL_REPLIED' || event === 'LINKEDIN_REPLIED') { console.log(`${contact.email ?? contact.linkedinUrlProfile} replied (${sentiment?.classification}): ${reply}`); } }); app.listen(3000); ``` **Python** ```python from flask import Flask, request app = Flask(__name__) # Keep the path hard to guess: deliveries are not signed. @app.post("/emelia/9f2c4e1a7b") def emelia_webhook(): payload = request.get_json(force=True) if payload.get("event") in ("MAIL_REPLIED", "LINKEDIN_REPLIED"): contact = payload.get("contact", {}) print(contact.get("email"), "replied:", payload.get("reply")) return "", 200 ``` ## Delivery rules Build your handler for these rules: * **No signature.** Deliveries carry `Content-Type: application/json` and nothing else: no secret, no signature header. Use a URL that is hard to guess, and treat the payload as a hint: confirm important facts with the API before acting on them. * **No retry on HTTP errors.** Your response is ignored. A few events are retried on network errors only. An endpoint that is down misses the events. * **No ordering.** Events come from different services: an `OPENED` can arrive after the `MAIL_REPLIED`. * **No event id.** To deduplicate, build a key from `event`, `campaign`, the contact and `step`. A URL registered twice on a campaign receives everything twice. * **No timeout on Emelia's side**, but answer in a few seconds anyway. The [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) has its own, separate webhooks (`FINISHED` and `ERROR`). --- # List webhooks Source: https://docs.emelia.io/docs/webhooks/list-webhooks Guide: [Webhooks](https://docs.emelia.io/docs/webhooks.md) `GET https://api.emelia.io/webhook` Returns the activity webhooks registered on the campaigns of the authenticated account: legacy email campaigns (`typeOfCampaign: email`), legacy LinkedIn campaigns (`linkedin`) and advanced campaigns (`advanced`), whatever their status (FINISHED and ARCHIVED included). There is one entry per registration and per campaign: a URL registered with `ALL_CAMPAIGNS` appears once for each campaign it was copied to, and the part kept for future campaigns is not listed. A URL registered twice on a campaign appears twice. Registrations with an empty event list are skipped. `status` is always `active`. **Authentication.** API key in `Authorization` (raw or `Bearer `), per key rate limit applies. **Errors.** 401 `Authentication required` for a missing or unknown key. 401 `You must be logged in` is also returned when reading the webhooks fails for any internal reason. Exceeding the per key rate limit returns 500 `Internal server error`, not 429. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/webhook" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/webhook', { 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/webhook", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Webhooks of the account. | Field | Type | Required | Description | |---|---|---|---| | `webhooks` | array of object | yes | | | `webhooks[].id` | string | no | Registration ID (use it with the test endpoints). | | `webhooks[].typeOfCampaign` | enum: `email`, `linkedin`, `advanced` | no | Campaign family. `advanced` = current multichannel campaigns (the default campaigns of the app and of the Campaigns endpoints). `email` = legacy email only campaigns. `linkedin` = legacy LinkedIn only campaigns. | | `webhooks[].campaignId` | string | no | | | `webhooks[].campaignName` | string | no | | | `webhooks[].url` | string | no | | | `webhooks[].events` | array of string | no | | | `webhooks[].status` | string | no | Always `active`: webhooks are never disabled automatically. | ```json { "webhooks": [ { "id": "6702c1d4e5f6a7b8c9d0e1f2", "typeOfCampaign": {}, "campaignId": "6651f0c2a1b2c3d4e5f60718", "campaignName": "Acme Q4 multichannel", "url": "https://hooks.acme.com/emelia/9f2c4e1a7b", "events": [ "MAIL_REPLIED", "LINKEDIN_REPLIED" ], "status": "string" } ] } ``` ### 401: Missing or unknown key, or internal error while reading webhooks. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 429: Session token rate limit reached (session tokens only; API keys never get 429, see 500). | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 500: Internal error. Also returned when the per API key rate limit (30, 100, 300 or 1000 requests per minute depending on the plan) is exceeded. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` --- # Activity on an advanced (multichannel) campaign Source: https://docs.emelia.io/docs/webhooks/campaign-activity Guide: [Webhooks](https://docs.emelia.io/docs/webhooks.md) Webhook event `advancedCampaignActivity`: Emelia sends an HTTP POST request to your URL. Sent to URLs registered with `type: advanced`. - `EMAIL_SENT`: email step sent (the documented name `MAIL_SENT` never fires). - `OPENED`, `CLICKED`, `UNSUBSCRIBED`: same tracking rules as legacy email, delivered 30 to 60 seconds later; contact without `id`, no `sender`. - `MAIL_REPLIED`: first email reply, with `reply`, `sentiment`, `sender`, `senderName`, `messageId`, `campaignId`. - `BOUNCED`: bounce detected; no `campaign` field, `step` 0. - `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT` (text in `content`), `INMAIL_SENT`, `LIKED`: LinkedIn actions. - `LINKEDIN_REPLIED`: LinkedIn reply with `reply` and `sentiment`; `campaignId`, `linkedinUrlProfile` and `linkedinId` only when detected in real time. - `WHATSAPP_MESSAGE_SENT`, `WHATSAPP_REPLIED`: WhatsApp message sent, reply received (`reply`). - `CAMPAIGN_ENDING`: see the campaignEnding webhook. Never sent: `SENT`, `MAIL_SENT`, `FOLLOWED`, `TASK_COMPLETED`, `TASK_FAILED`, `FAILED_ACTION`, `EXCLUDED`, `RE_REPLY_EMAIL`, `RE_REPLY_LINKEDIN`. Only the real time LinkedIn path retries (once, network errors). **Delivery.** One `POST` per event and per registered URL, header `Content-Type: application/json`, JSON body, no signature or secret header, default User-Agent of the HTTP library. No timeout is set. Your response (status and body) is ignored: an HTTP error is never retried. Network errors are retried only for some events (see each list). No event ID, no ordering guarantee, no replay; a URL registered twice receives everything twice. Absent fields are omitted, never null. ## Payload | Field | Type | Required | Description | |---|---|---|---| | `event` | enum: `EMAIL_SENT`, `OPENED`, `CLICKED`, `MAIL_REPLIED`, `BOUNCED`, `UNSUBSCRIBED`, `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `LIKED`, `LINKEDIN_REPLIED`, `WHATSAPP_MESSAGE_SENT`, `WHATSAPP_REPLIED`, `CAMPAIGN_ENDING` | yes | Events delivered for advanced (multichannel) campaigns (`type: advanced`). The email sent event is `EMAIL_SENT` (not `SENT`, not `MAIL_SENT`). `CAMPAIGN_ENDING` is delivered with a different body (see the campaignEnding webhook). | | `campaign` | string | no | Campaign name. Missing on BOUNCED (bug). | | `campaignId` | string | no | Only on MAIL_REPLIED and on LinkedIn replies or acceptances detected in real time. | | `senderName` | string | no | MAIL_REPLIED only. | | `sender` | string | no | MAIL_REPLIED only: address of the Emelia mailbox that received the reply. | | `contact` | object | yes | Contact of an advanced campaign: legacy custom fields (`custom`) first, then the standard fields. The set depends on the emitter: most events carry `id`, `firstName`, `lastName`, `email`, `company`; OPENED, CLICKED and UNSUBSCRIBED carry `linkedinUrlProfile` instead of `id`; replies and acceptances detected in real time add `linkedinUrlProfile` and `linkedinId`. No phone number. `company` comes only from a custom field whose name contains `company`, not from the lead company name. | | `contact.id` | string | no | Absent on OPENED, CLICKED and UNSUBSCRIBED. | | `contact.firstName` | string | no | | | `contact.lastName` | string | no | | | `contact.email` | string | no | | | `contact.linkedinUrlProfile` | string | no | Only on OPENED, CLICKED, UNSUBSCRIBED and real time LinkedIn replies or acceptances. | | `contact.linkedinId` | string | no | Only on real time LinkedIn replies or acceptances. | | `contact.company` | string | no | | | `date` | string (date-time) | yes | ISO 8601 UTC. Processing time for most events; message time for LinkedIn and WhatsApp replies; 30 to 60 seconds after the fact for OPENED, CLICKED and UNSUBSCRIBED. | | `step` | integer | yes | Indicative only. Actions: position of the step in its branch (0 for the first step after the start). OPENED, CLICKED, UNSUBSCRIBED: same but counting the start node (EMAIL_SENT 0 then OPENED 1 for the same email). MAIL_REPLIED: index among the email steps. BOUNCED, WHATSAPP_REPLIED, LINKEDIN_REPLIED and most ACCEPTED: 0. | | `reply` | string | no | MAIL_REPLIED, LINKEDIN_REPLIED, WHATSAPP_REPLIED. | | `sentiment` | object | no | AI classification of a reply. Present only when reply classification is enabled on the account and AI credits remain. Classified by Jev, with Gemini as fallback; only the Gemini fallback can add `additionalData`. | | `sentiment.classification` | enum: `INTERESTED`, `NOT_INTERESTED`, `NEUTRAL`, `OUT_OF_OFFICE`, `HOSTILE`, `DELEGATION`, `INCOMPREHENSION`, `QUESTION_ASK` | no | | | `sentiment.score` | number | no | Confidence of the classification. Range: 0 to 1. | | `sentiment.additionalData` | object | no | Gemini fallback only: `delegationEmail` for DELEGATION, `returnDate` (DD/MM/YYYY) for OUT_OF_OFFICE. | | `sentiment.additionalData.delegationEmail` | string | no | | | `sentiment.additionalData.returnDate` | string | no | | | `content` | string | no | MESSAGE_SENT only: text of the LinkedIn message sent. | | `messageId` | string | no | MAIL_REPLIED only. | | `link` | string | no | CLICKED only. | **EMAIL_SENT** ```json { "event": "EMAIL_SENT", "campaign": "Acme Q4 multichannel", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T08:15:02.123Z", "step": 0 } ``` **OPENED (no contact id, no sender)** ```json { "event": "OPENED", "campaign": "Acme Q4 multichannel", "contact": { "companyName": "Acme", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T09:01:45.000Z", "step": 1 } ``` **MAIL_REPLIED** ```json { "event": "MAIL_REPLIED", "campaign": "Acme Q4 multichannel", "campaignId": "6651f0c2a1b2c3d4e5f60718", "senderName": "Jane Smith", "sender": "jane@acme-mail.com", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-13T10:20:00.000Z", "step": 0, "reply": "Hi Jane, sounds interesting. Can we talk on Tuesday?", "sentiment": { "classification": "INTERESTED", "score": 0.92 }, "messageId": "" } ``` **INVITED** ```json { "event": "INVITED", "campaign": "Acme Q4 multichannel", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T08:40:00.000Z", "step": 1 } ``` **MESSAGE_SENT (with content)** ```json { "event": "MESSAGE_SENT", "campaign": "Acme Q4 multichannel", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "company": "Acme" }, "content": "Hi John, thanks for connecting. Quick question about Acme...", "date": "2026-10-14T08:30:00.000Z", "step": 3 } ``` **LINKEDIN_REPLIED detected in real time** ```json { "event": "LINKEDIN_REPLIED", "campaign": "Acme Q4 multichannel", "campaignId": "6651f0c2a1b2c3d4e5f60718", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "company": "Acme", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "linkedinId": "ACoAAB1x2y3zAcmeExample" }, "date": "2026-10-15T07:45:12.000Z", "step": 0, "reply": "Hi, happy to chat next week.", "sentiment": { "classification": "INTERESTED", "score": 0.92 } } ``` **WHATSAPP_REPLIED** ```json { "event": "WHATSAPP_REPLIED", "campaign": "Acme Q4 multichannel", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-16T12:00:00.000Z", "step": 0, "reply": "Sure, call me tomorrow." } ``` **BOUNCED (no campaign name)** ```json { "event": "BOUNCED", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T08:16:30.000Z", "step": 0 } ``` --- # Campaign finished Source: https://docs.emelia.io/docs/webhooks/campaign-finished Guide: [Webhooks](https://docs.emelia.io/docs/webhooks.md) Webhook event `campaignEnding`: Emelia sends an HTTP POST request to your URL. Sent once to URLs whose events include `CAMPAIGN_ENDING`, when a campaign is closed automatically by the daily job (04:00 server time). In the current code it is only delivered for advanced campaigns that are not `alwaysOn`; the email and LinkedIn variants are accepted at registration but never sent. Marking a campaign as finished by hand (app or API) does not trigger it, nor does archiving. Single attempt, errors ignored. ## Payload | Field | Type | Required | Description | |---|---|---|---| | `date` | string (date-time) | yes | Time of the automatic closing (daily job at 04:00 server time). | | `campaign` | string | yes | Campaign name. | | `id` | string | yes | Campaign ID. | **ending** ```json { "date": "2026-10-20T04:00:12.000Z", "campaign": "Acme Q4 multichannel", "id": "6651f0c2a1b2c3d4e5f60718" } ``` --- # Activity on a legacy email campaign Source: https://docs.emelia.io/docs/webhooks/legacy-email-activity Guide: [Webhooks](https://docs.emelia.io/docs/webhooks.md) Webhook event `emailCampaignActivity`: Emelia sends an HTTP POST request to your URL. Sent to URLs registered with `type: email` (or without `type`). - `SENT`: right after a sequence email is sent (test sends excluded). Network errors retried up to 5 times; the send waits for your endpoint. - `OPENED`: tracking pixel loaded, at most once per email and contact, delivered 30 to 60 seconds later. Needs open tracking. - `CLICKED`: tracked link clicked, at most once per link and email, delivered 30 to 60 seconds later; two clicks of the same contact on the same step queued within that delay can be merged into the last one. With open tracking on, a click is ignored until an open was recorded. - `REPLIED`: first reply of the contact detected in the mailbox (later replies are not sent), with reply text, AI classification, sending mailbox and Message-ID. Network errors retried up to 5 times. - `BOUNCED`: bounce detected (non delivery report or refusal at send time). No `campaign` field and `step` always 0. Network errors retried up to 5 times. - `UNSUBSCRIBED`: the contact confirms on the unsubscribe page (or a one click unsubscribe POST is received); once per contact. - `INTERESTED`: contact marked as interested by a user in the legacy web app (not through the REST API). - `CAMPAIGN_ENDING`: accepted at registration, never sent for email campaigns. **Delivery.** One `POST` per event and per registered URL, header `Content-Type: application/json`, JSON body, no signature or secret header, default User-Agent of the HTTP library. No timeout is set. Your response (status and body) is ignored: an HTTP error is never retried. Network errors are retried only for some events (see each list). No event ID, no ordering guarantee, no replay; a URL registered twice receives everything twice. Absent fields are omitted, never null. ## Payload | Field | Type | Required | Description | |---|---|---|---| | `event` | enum: `SENT`, `OPENED`, `CLICKED`, `REPLIED`, `BOUNCED`, `UNSUBSCRIBED`, `INTERESTED`, `CAMPAIGN_ENDING` | yes | Events delivered for legacy email campaigns (`type: email`). `CAMPAIGN_ENDING` is accepted but never delivered for email campaigns in the current code. | | `campaign` | string | no | Campaign name. Missing on BOUNCED (bug). | | `campaignId` | string | no | REPLIED only. | | `sender` | string | no | Address of the Emelia sending mailbox (the one that received the reply). REPLIED, OPENED and CLICKED only. | | `senderName` | string | no | REPLIED only. | | `contact` | object | yes | Contact of a legacy email campaign. Starts with every custom field of the contact (any key), then the standard fields, which win on name clashes. `company` is the value of the first custom field whose name contains `company` (case insensitive), absent otherwise. No contact ID. | | `contact.firstName` | string | no | | | `contact.lastName` | string | no | | | `contact.phoneNumber` | string | no | | | `contact.email` | string (email) | no | | | `contact.company` | string | no | | | `date` | string (date-time) | yes | ISO 8601 UTC. Time at which Emelia processed the event: send time, detection time for replies (not the reply's own date for email), processing time 30 to 60 seconds after the open or click for OPENED and CLICKED. | | `step` | integer | yes | 0 based index of the sequence step. Always 0 for BOUNCED and INTERESTED. Can be -1 when the step cannot be resolved. | | `reply` | string | no | REPLIED only. Plain text of the reply (HTML when no text part), usually including the quoted thread. | | `sentiment` | object | no | AI classification of a reply. Present only when reply classification is enabled on the account and AI credits remain. Classified by Jev, with Gemini as fallback; only the Gemini fallback can add `additionalData`. | | `sentiment.classification` | enum: `INTERESTED`, `NOT_INTERESTED`, `NEUTRAL`, `OUT_OF_OFFICE`, `HOSTILE`, `DELEGATION`, `INCOMPREHENSION`, `QUESTION_ASK` | no | | | `sentiment.score` | number | no | Confidence of the classification. Range: 0 to 1. | | `sentiment.additionalData` | object | no | Gemini fallback only: `delegationEmail` for DELEGATION, `returnDate` (DD/MM/YYYY) for OUT_OF_OFFICE. | | `sentiment.additionalData.delegationEmail` | string | no | | | `sentiment.additionalData.returnDate` | string | no | | | `messageId` | string | no | REPLIED only. Message-ID of the reply, usable to answer in the same thread. | | `link` | string | no | CLICKED only. Original URL of the clicked link. | **SENT** ```json { "event": "SENT", "campaign": "Acme Q4 outbound", "contact": { "companyName": "Acme", "industry": "Software", "firstName": "John", "lastName": "Doe", "phoneNumber": "+33612345678", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T08:15:02.123Z", "step": 0 } ``` **OPENED (sender = sending mailbox)** ```json { "event": "OPENED", "campaign": "Acme Q4 outbound", "sender": "jane@acme-mail.com", "contact": { "companyName": "Acme", "industry": "Software", "firstName": "John", "lastName": "Doe", "phoneNumber": "+33612345678", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T09:01:45.000Z", "step": 0 } ``` **CLICKED** ```json { "event": "CLICKED", "campaign": "Acme Q4 outbound", "sender": "jane@acme-mail.com", "contact": { "companyName": "Acme", "industry": "Software", "firstName": "John", "lastName": "Doe", "phoneNumber": "+33612345678", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T09:03:10.000Z", "step": 0, "link": "https://www.acme.com/pricing" } ``` **REPLIED** ```json { "event": "REPLIED", "campaign": "Acme Q4 outbound", "campaignId": "6650aa10b2c3d4e5f6071829", "sender": "jane@acme-mail.com", "senderName": "Jane Smith", "contact": { "companyName": "Acme", "industry": "Software", "firstName": "John", "lastName": "Doe", "phoneNumber": "+33612345678", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-13T10:20:00.000Z", "step": 1, "reply": "Hi Jane, sounds interesting. Can we talk on Tuesday?", "sentiment": { "classification": "INTERESTED", "score": 0.92 }, "messageId": "" } ``` **BOUNCED (no campaign name, step always 0)** ```json { "event": "BOUNCED", "contact": { "companyName": "Acme", "industry": "Software", "firstName": "John", "lastName": "Doe", "phoneNumber": "+33612345678", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T08:16:30.000Z", "step": 0 } ``` **UNSUBSCRIBED** ```json { "event": "UNSUBSCRIBED", "campaign": "Acme Q4 outbound", "contact": { "companyName": "Acme", "industry": "Software", "firstName": "John", "lastName": "Doe", "phoneNumber": "+33612345678", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-12T11:00:00.000Z", "step": 0 } ``` **INTERESTED (marked by a user)** ```json { "event": "INTERESTED", "campaign": "Acme Q4 outbound", "contact": { "companyName": "Acme", "industry": "Software", "firstName": "John", "lastName": "Doe", "phoneNumber": "+33612345678", "email": "john.doe@acme.com", "company": "Acme" }, "date": "2026-10-13T14:00:00.000Z", "step": 0 } ``` --- # Activity on a legacy LinkedIn campaign Source: https://docs.emelia.io/docs/webhooks/legacy-linkedin-activity Guide: [Webhooks](https://docs.emelia.io/docs/webhooks.md) Webhook event `linkedinCampaignActivity`: Emelia sends an HTTP POST request to your URL. Sent to URLs registered with `type: linkedin` or `linkedin: true`. - `VISITED`: profile visited. - `INVITED`: invitation sent. When the contact was already a connection, `INVITED` and `ACCEPTED` are sent together. - `ACCEPTED`: acceptance detected (connections polling, inferred from a reply, or real time LinkedIn notification). - `MESSAGE_SENT`: message sent, text in `content`. - `INMAIL_SENT`: InMail sent. - `REPLIED`: first reply detected, with `reply` and `sentiment`. `LIKED`, `FOLLOWED` and `CAMPAIGN_ENDING` are never sent for these campaigns. Most events come from the LinkedIn worker (contact without `id` nor `email`). Replies and acceptances detected in real time come from the API server and add `campaignId`, `contact.id` and `contact.email`; that path retries once on network errors, the worker never retries. **Delivery.** One `POST` per event and per registered URL, header `Content-Type: application/json`, JSON body, no signature or secret header, default User-Agent of the HTTP library. No timeout is set. Your response (status and body) is ignored: an HTTP error is never retried. Network errors are retried only for some events (see each list). No event ID, no ordering guarantee, no replay; a URL registered twice receives everything twice. Absent fields are omitted, never null. ## Payload | Field | Type | Required | Description | |---|---|---|---| | `event` | enum: `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `REPLIED` | yes | Events delivered for legacy LinkedIn campaigns (`type: linkedin`). `LIKED`, `FOLLOWED` and `CAMPAIGN_ENDING` are never delivered for these campaigns. | | `campaign` | string | yes | Campaign name. | | `campaignId` | string | no | Only on replies and acceptances detected in real time. | | `contact` | object | yes | Contact of a legacy LinkedIn campaign: custom fields first, then the standard fields. `id` (Emelia contact ID) and `email` are only present when the event was detected in real time by the API server (replies and acceptances); events from the LinkedIn worker never have them. | | `contact.id` | string | no | Real time replies and acceptances only. | | `contact.firstName` | string | no | | | `contact.lastName` | string | no | | | `contact.email` | string | no | Real time replies and acceptances only. | | `contact.linkedinUrlProfile` | string (uri) | no | | | `contact.linkedinId` | string | no | LinkedIn member ID. | | `contact.company` | string | no | | | `date` | string (date-time) | yes | ISO 8601 UTC. Action time, or message time for replies. | | `step` | integer | yes | 0 based index of the sequence step (-1 when not resolved). | | `reply` | string | no | REPLIED only. | | `sentiment` | object | no | AI classification of a reply. Present only when reply classification is enabled on the account and AI credits remain. Classified by Jev, with Gemini as fallback; only the Gemini fallback can add `additionalData`. | | `sentiment.classification` | enum: `INTERESTED`, `NOT_INTERESTED`, `NEUTRAL`, `OUT_OF_OFFICE`, `HOSTILE`, `DELEGATION`, `INCOMPREHENSION`, `QUESTION_ASK` | no | | | `sentiment.score` | number | no | Confidence of the classification. Range: 0 to 1. | | `sentiment.additionalData` | object | no | Gemini fallback only: `delegationEmail` for DELEGATION, `returnDate` (DD/MM/YYYY) for OUT_OF_OFFICE. | | `sentiment.additionalData.delegationEmail` | string | no | | | `sentiment.additionalData.returnDate` | string | no | | | `content` | string | no | MESSAGE_SENT only: text of the message sent. | **VISITED** ```json { "event": "VISITED", "campaign": "Acme LinkedIn Q4", "contact": { "companyName": "Acme", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "company": "Acme", "linkedinId": "ACoAAB1x2y3zAcmeExample" }, "date": "2026-10-12T08:30:00.000Z", "step": 0 } ``` **MESSAGE_SENT (with content)** ```json { "event": "MESSAGE_SENT", "campaign": "Acme LinkedIn Q4", "contact": { "companyName": "Acme", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "company": "Acme", "linkedinId": "ACoAAB1x2y3zAcmeExample" }, "date": "2026-10-14T08:30:00.000Z", "step": 2, "content": "Hi John, thanks for connecting. Quick question about Acme..." } ``` **REPLIED from the LinkedIn worker** ```json { "event": "REPLIED", "campaign": "Acme LinkedIn Q4", "contact": { "companyName": "Acme", "firstName": "John", "lastName": "Doe", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "company": "Acme", "linkedinId": "ACoAAB1x2y3zAcmeExample" }, "date": "2026-10-15T07:45:12.000Z", "step": 2, "reply": "Hi, happy to chat next week.", "sentiment": { "classification": "INTERESTED", "score": 0.92 } } ``` **REPLIED detected in real time (API server)** ```json { "event": "REPLIED", "campaign": "Acme LinkedIn Q4", "campaignId": "6650aa10b2c3d4e5f6071829", "contact": { "companyName": "Acme", "id": "6703d2e5f6a7b8c9d0e1f2a3", "firstName": "John", "lastName": "Doe", "email": "john.doe@acme.com", "linkedinUrlProfile": "https://www.linkedin.com/in/john-doe-acme", "linkedinId": "ACoAAB1x2y3zAcmeExample", "company": "Acme" }, "date": "2026-10-15T07:45:12.000Z", "step": 2, "reply": "Hi, happy to chat next week.", "sentiment": { "classification": "INTERESTED", "score": 0.92 } } ``` --- # Email providers > List and connect the sending mailboxes of your Emelia account, change their sender name, enable their warmup and run a deliverability diagnosis by API. Source: https://docs.emelia.io/docs/email-providers An **email provider** is a sending mailbox connected to Emelia: a Google or Microsoft account, or any SMTP and IMAP mailbox. Campaigns send from them, through the [identities](https://docs.emelia.io/docs/campaigns.md#4-choose-the-senders) of each campaign. ## List your mailboxes [`GET /email-providers`](https://docs.emelia.io/docs/email-providers/list-email-providers.md) **curl** ```bash curl https://api.emelia.io/email-providers \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/email-providers', { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { providers } = await res.json(); const usable = providers.filter((p) => !p.disconnected && !p.disabled); console.log(usable.map((p) => `${p._id} ${p.senderEmail}`)); ``` **Python** ```python import os import requests res = requests.get( "https://api.emelia.io/email-providers", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) usable = [p for p in res.json()["providers"] if not p.get("disconnected") and not p.get("disabled")] print([(p["_id"], p["senderEmail"]) for p in usable]) ``` Each mailbox comes with the campaigns that use it. Passwords are always masked (`********`). There is no pagination: you get every mailbox in one response. The `_id` is what you put in `identities[].email` of a campaign. ## Connect an SMTP and IMAP mailbox [`POST /email-providers`](https://docs.emelia.io/docs/email-providers/connect-mailbox.md) **curl** ```bash curl -X POST https://api.emelia.io/email-providers \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "senderName": "Marie Martin", "senderEmail": "marie.martin@yourcompany.com", "emailType": "SMTP", "smtp": { "login": "marie.martin@yourcompany.com", "password": "app-password", "server": "smtp.yourcompany.com", "port": 465, "ssl": true }, "imap": { "login": "marie.martin@yourcompany.com", "password": "app-password", "server": "imap.yourcompany.com", "port": 993, "ssl": true } }' ``` **JavaScript** ```js await fetch('https://api.emelia.io/email-providers', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ senderName: 'Marie Martin', senderEmail: 'marie.martin@yourcompany.com', emailType: 'SMTP', smtp: { login: 'marie.martin@yourcompany.com', password: process.env.SMTP_PASSWORD, server: 'smtp.yourcompany.com', port: 465, ssl: true }, imap: { login: 'marie.martin@yourcompany.com', password: process.env.IMAP_PASSWORD, server: 'imap.yourcompany.com', port: 993, ssl: true }, }), }); ``` **Python** ```python requests.post( "https://api.emelia.io/email-providers", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "senderName": "Marie Martin", "senderEmail": "marie.martin@yourcompany.com", "emailType": "SMTP", "smtp": {"login": "marie.martin@yourcompany.com", "password": os.environ["SMTP_PASSWORD"], "server": "smtp.yourcompany.com", "port": 465, "ssl": True}, "imap": {"login": "marie.martin@yourcompany.com", "password": os.environ["IMAP_PASSWORD"], "server": "imap.yourcompany.com", "port": 993, "ssl": True}, }, ) ``` Emelia tests the SMTP and IMAP credentials before saving the mailbox. The number of mailboxes is limited by your plan. Google and Microsoft accounts are connected in the Emelia app, with their own sign in. To fix the credentials of a disconnected mailbox, use [Update a mailbox](https://docs.emelia.io/docs/email-providers/update-mailbox.md): masked passwords (`********`) are kept as stored. [Change the sender name](https://docs.emelia.io/docs/email-providers/change-sender-name.md) works on every type of mailbox, without a new test. ## Warmup Warmup builds the sending reputation of a mailbox before and while it prospects. [`GET /email-providers/warmup`](https://docs.emelia.io/docs/email-providers/list-warmups.md) [`POST /email-providers/warmup/enable`](https://docs.emelia.io/docs/email-providers/enable-warmup.md) **curl** ```bash curl -X POST https://api.emelia.io/email-providers/warmup/enable \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"providerId": "6650f1a2b839d80011c5a0aa"}' ``` **JavaScript** ```js await fetch('https://api.emelia.io/email-providers/warmup/enable', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ providerId: '6650f1a2b839d80011c5a0aa' }), }); ``` **Python** ```python requests.post( "https://api.emelia.io/email-providers/warmup/enable", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={"providerId": "6650f1a2b839d80011c5a0aa"}, ) ``` * Enabling the warmup needs an active subscription. `providerId` takes the id of the mailbox or of its warmup. * Some hosts cannot be warmed up: the call fails with `WARMUP_PROVIDER_UNSUPPORTED`. * `GET /email-providers/warmup` returns the state of every mailbox (running or not, statistics, diagnosis), without pagination: a `page` parameter is ignored. The `_id` of an item is the warmup id. * To turn a warmup off, use the Emelia app. ## Deliverability diagnosis [`POST /email-providers/warmup/check-health`](https://docs.emelia.io/docs/email-providers/run-deliverability-check.md) Asks for a new DNS, sending and blacklist diagnosis of a mailbox, from its warmup id (`{"warmupId": "..."}`). The result appears a few minutes later in `health` of `GET /email-providers/warmup`. This route answers its errors as `{"success": false, "message": "..."}`. ## Remove a mailbox [`DELETE /email-providers/{providerId}`](https://docs.emelia.io/docs/email-providers/delete-mailbox.md) Removes the mailbox from your account. Detach it from your running campaigns first, with [Set the sender identities](https://docs.emelia.io/docs/campaigns/set-sender-identities.md). --- # List email providers Source: https://docs.emelia.io/docs/email-providers/list-email-providers Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `GET https://api.emelia.io/email-providers` All sending mailboxes of the account with the campaigns that use them. Passwords are masked. No pagination. Their `_id` is what you put in `identities[].email` of a campaign. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `filter` | enum: `disconnected` | no | Only disconnected mailboxes. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/email-providers" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers', { 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/email-providers", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `providers` | array of object | no | | | `providers[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `providers[].userId` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `providers[].senderName` | string | no | | | `providers[].senderEmail` | string | no | | | `providers[].senderPassword` | string | no | | | `providers[].emailType` | enum: `GOOGLE`, `OFFICE`, `EXCHANGE`, `SMTP`, `GOOGLEIMAP` | no | | | `providers[].smtp` | object | no | | | `providers[].imap` | object | no | | | `providers[].customDomain` | object | no | Custom tracking domain, when set. | | `providers[].signature` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `providers[].warmup` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `providers[].health` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `providers[].disabled` | boolean | no | True when the paid slot of the mailbox expired. | | `providers[].disconnected` | boolean | no | True when the credentials or the OAuth grant stopped working: reconnect the mailbox. | | `providers[].campaigns` | array of object | no | Non archived campaigns using this mailbox. | ```json { "success": true, "providers": [ { "_id": "6650f1a2b839d80011c5a0f1", "userId": "6650f1a2b839d80011c5a0f1", "senderName": "Marie Martin", "senderEmail": "marie.martin@yourcompany.com", "senderPassword": "********", "emailType": "GOOGLE", "smtp": { "login": "marie.martin@yourcompany.com", "password": "********", "server": "smtp.yourcompany.com", "port": 587, "ssl": true }, "imap": { "login": "marie.martin@yourcompany.com", "password": "********", "server": "imap.yourcompany.com", "port": 993, "ssl": true }, "customDomain": { "_id": "6650f1a2b839d80011c5a0f1", "domainName": "track.yourcompany.com", "dateAdded": "2026-10-06T09:30:00.000Z" }, "signature": "6650f1a2b839d80011c5a0f1", "warmup": "6650f1a2b839d80011c5a0f1", "health": "6650f1a2b839d80011c5a0f1", "disabled": false, "disconnected": false, "campaigns": [ { "_id": "6650f1a2b839d800bb200e24", "name": "Q4 outreach: SaaS founders", "type": "EMAIL" } ] } ] } ``` ### 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" } ``` --- # Connect an SMTP/IMAP mailbox Source: https://docs.emelia.io/docs/email-providers/connect-mailbox Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `POST https://api.emelia.io/email-providers` Adds a mailbox after testing its SMTP and IMAP credentials. The number of mailboxes is limited by your subscription (1 plus the additional mailboxes you pay for). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. SMTP and IMAP credentials of the mailbox. The credentials are tested before saving. For Google and Microsoft accounts prefer the OAuth connection in the app. | Field | Type | Required | Description | |---|---|---|---| | `senderName` | string | no | | | `senderEmail` | string (email) | yes | | | `senderPassword` | string | no | Used when `smtp.password` is empty. | | `emailType` | enum: `GOOGLE`, `OFFICE`, `EXCHANGE`, `SMTP`, `GOOGLEIMAP` | yes | | | `smtp` | object | no | | | `smtp.login` | string | no | | | `smtp.password` | string | no | | | `smtp.server` | string | yes | | | `smtp.port` | integer | yes | | | `smtp.ssl` | boolean | no | | | `imap` | object | no | | | `imap.login` | string | no | | | `imap.password` | string | no | | | `imap.server` | string | yes | | | `imap.port` | integer | yes | | | `imap.ssl` | boolean | no | | | `signature` | string | no | | | `domainName` | string | no | Optional custom tracking domain to attach. | ```json { "senderName": "Marie Martin", "senderEmail": "marie@acme.com", "emailType": "SMTP", "smtp": { "login": "marie@acme.com", "password": "app-password", "server": "smtp.acme.com", "port": 465, "ssl": true }, "imap": { "login": "marie@acme.com", "password": "app-password", "server": "imap.acme.com", "port": 993, "ssl": true } } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/email-providers" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "senderName": "Marie Martin", "senderEmail": "marie@acme.com", "emailType": "SMTP", "smtp": { "login": "marie@acme.com", "password": "app-password", "server": "smtp.acme.com", "port": 465, "ssl": true }, "imap": { "login": "marie@acme.com", "password": "app-password", "server": "imap.acme.com", "port": 993, "ssl": true } }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "senderName": "Marie Martin", "senderEmail": "marie@acme.com", "emailType": "SMTP", "smtp": { "login": "marie@acme.com", "password": "app-password", "server": "smtp.acme.com", "port": 465, "ssl": true }, "imap": { "login": "marie@acme.com", "password": "app-password", "server": "imap.acme.com", "port": 993, "ssl": true } }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/email-providers", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "senderName": "Marie Martin", "senderEmail": "marie@acme.com", "emailType": "SMTP", "smtp": { "login": "marie@acme.com", "password": "app-password", "server": "smtp.acme.com", "port": 465, "ssl": True }, "imap": { "login": "marie@acme.com", "password": "app-password", "server": "imap.acme.com", "port": 993, "ssl": True } }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "id": "6650f1a2b839d80011c5a0aa" } ``` ### 400: Invalid request or credentials rejected. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Update an SMTP/IMAP mailbox Source: https://docs.emelia.io/docs/email-providers/update-mailbox Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `PATCH https://api.emelia.io/email-providers/{providerId}` Updates the credentials of an SMTP or Google IMAP mailbox, retests them and marks the mailbox as connected again. Masked passwords (`********`) are replaced by the stored values before the test. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `providerId` | string | yes | Email provider id. | ## Request body Content type: `application/json`, required. SMTP and IMAP credentials of the mailbox. The credentials are tested before saving. For Google and Microsoft accounts prefer the OAuth connection in the app. | Field | Type | Required | Description | |---|---|---|---| | `senderName` | string | no | | | `senderEmail` | string (email) | yes | | | `senderPassword` | string | no | Used when `smtp.password` is empty. | | `emailType` | enum: `GOOGLE`, `OFFICE`, `EXCHANGE`, `SMTP`, `GOOGLEIMAP` | yes | | | `smtp` | object | no | | | `smtp.login` | string | no | | | `smtp.password` | string | no | | | `smtp.server` | string | yes | | | `smtp.port` | integer | yes | | | `smtp.ssl` | boolean | no | | | `imap` | object | no | | | `imap.login` | string | no | | | `imap.password` | string | no | | | `imap.server` | string | yes | | | `imap.port` | integer | yes | | | `imap.ssl` | boolean | no | | | `signature` | string | no | | | `domainName` | string | no | Optional custom tracking domain to attach. | ```json { "senderName": "Marie Martin", "senderEmail": "marie.martin@yourcompany.com", "senderPassword": "your-mailbox-password", "emailType": "GOOGLE", "smtp": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "smtp.yourcompany.com", "port": 587, "ssl": true }, "imap": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "imap.yourcompany.com", "port": 993, "ssl": true }, "domainName": "track.yourcompany.com" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "senderName": "Marie Martin", "senderEmail": "marie.martin@yourcompany.com", "senderPassword": "your-mailbox-password", "emailType": "GOOGLE", "smtp": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "smtp.yourcompany.com", "port": 587, "ssl": true }, "imap": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "imap.yourcompany.com", "port": 993, "ssl": true }, "domainName": "track.yourcompany.com" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "senderName": "Marie Martin", "senderEmail": "marie.martin@yourcompany.com", "senderPassword": "your-mailbox-password", "emailType": "GOOGLE", "smtp": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "smtp.yourcompany.com", "port": 587, "ssl": true }, "imap": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "imap.yourcompany.com", "port": 993, "ssl": true }, "domainName": "track.yourcompany.com" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "senderName": "Marie Martin", "senderEmail": "marie.martin@yourcompany.com", "senderPassword": "your-mailbox-password", "emailType": "GOOGLE", "smtp": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "smtp.yourcompany.com", "port": 587, "ssl": True }, "imap": { "login": "marie.martin@yourcompany.com", "password": "your-mailbox-password", "server": "imap.yourcompany.com", "port": 993, "ssl": True }, "domainName": "track.yourcompany.com" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "id": "6650f1a2b839d80011c5a0f1" } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Change the sender name Source: https://docs.emelia.io/docs/email-providers/change-sender-name Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `PATCH https://api.emelia.io/email-providers/{providerId}/sender-name` Updates only the sender name of a mailbox (any type), without retesting credentials. Note: Outlook always displays the name of the Microsoft account. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `providerId` | string | yes | Email provider id. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `senderName` | string | yes | | ```json { "senderName": "Marie from Acme" } ``` ## Example request **cURL** ```bash curl -X PATCH "https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa/sender-name" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "senderName": "Marie from Acme" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa/sender-name', { method: 'PATCH', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "senderName": "Marie from Acme" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.patch( "https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa/sender-name", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "senderName": "Marie from Acme" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | ```json { "success": true, "id": "6650f1a2b839d80011c5a0f1" } ``` ### 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" } ``` --- # Delete a mailbox Source: https://docs.emelia.io/docs/email-providers/delete-mailbox Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `DELETE https://api.emelia.io/email-providers/{providerId}` Removes the mailbox from the account. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `providerId` | string | yes | Email provider id. | ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/email-providers/6650f1a2b839d80011c5a0aa", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 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" } ``` --- # List warmups Source: https://docs.emelia.io/docs/email-providers/list-warmups Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `GET https://api.emelia.io/email-providers/warmup` Warmup state of every mailbox (a warmup record is created on the fly for mailboxes that have none). No pagination: a `page` parameter is ignored. The `_id` of an item is the warmup id. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/email-providers/warmup" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers/warmup', { 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/email-providers/warmup", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | any | no | | | `warmups` | array of object | no | | | `warmups[].email` | string | no | | | `warmups[]._id` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `warmups[].provider` | string | no | MongoDB ObjectId: 24 hexadecimal characters. | | `warmups[].running` | boolean | no | | | `warmups[].disabledReason` | string | no | | | `warmups[].stats` | object | no | | | `warmups[].health` | object or null | no | Last DNS and deliverability diagnosis, or null. | ```json { "success": true, "warmups": [ { "email": "claire.fontaine@nexora.io", "_id": "6650f1a2b839d80011c5a0f1", "provider": "6650f1a2b839d80011c5a0f1", "running": true, "stats": {}, "health": {} } ] } ``` ### 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" } ``` --- # Enable the warmup of a mailbox Source: https://docs.emelia.io/docs/email-providers/enable-warmup Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `POST https://api.emelia.io/email-providers/warmup/enable` Starts the warmup of a mailbox. Requires an active subscription. `providerId` accepts the email provider id or the warmup id. The mailbox connection is tested first. Some hosts are refused (`WARMUP_PROVIDER_UNSUPPORTED`). A warmup disabled for incompatibility can only be reactivated a few times. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `providerId` | string | yes | Email provider id (or warmup id). | ```json { "providerId": "6650f1a2b839d80011c5a0aa" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/email-providers/warmup/enable" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "providerId": "6650f1a2b839d80011c5a0aa" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers/warmup/enable', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "providerId": "6650f1a2b839d80011c5a0aa" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/email-providers/warmup/enable", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "providerId": "6650f1a2b839d80011c5a0aa" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid request. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 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" } ``` --- # Run a deliverability diagnosis Source: https://docs.emelia.io/docs/email-providers/run-deliverability-check Guide: [Email providers](https://docs.emelia.io/docs/email-providers.md) `POST https://api.emelia.io/email-providers/warmup/check-health` Asks for a new DNS, sending and blacklist diagnosis of a mailbox. Asynchronous: the result appears a few minutes later in `health` of `GET /email-providers/warmup`. Note: a missing `warmupId` returns `{"success": false, "message": ...}`, not the usual error format. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `warmupId` | string | yes | | ```json { "warmupId": "6650f1a2b839d80011c5a0cc" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/email-providers/warmup/check-health" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "warmupId": "6650f1a2b839d80011c5a0cc" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/email-providers/warmup/check-health', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "warmupId": "6650f1a2b839d80011c5a0cc" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/email-providers/warmup/check-health", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "warmupId": "6650f1a2b839d80011c5a0cc" }, ) print(response.json()) ``` ## Responses ### 200: Success. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Missing warmupId, or other error. ```json {} ``` ### 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" } ``` --- # LinkedIn scraper > Extract the people or companies of a LinkedIn or Sales Navigator search with the Emelia LinkedIn Scraper API, find their emails, download the CSV, and get them as an Emelia list. Source: https://docs.emelia.io/docs/linkedin-scraper The LinkedIn scraper extracts the people (or companies) of a LinkedIn search into a CSV, then optionally finds their professional emails. Every finished extraction also becomes an Emelia list, ready to attach to a campaign. > **Emelia V3.** The V3 flow (scrape into a list) is being documented. ## The flow 1. **Connect a LinkedIn account** for scraping, with the cookies of a browser session ([Connect an account](https://docs.emelia.io/docs/linkedin-scraper/connect-account.md)). The Emelia browser extension does it for you. 2. **Start an extraction** with the search URL ([Start an extraction](https://docs.emelia.io/docs/linkedin-scraper/start-extraction.md)). It runs in the background. 3. **Follow it**: poll [Get a scraper](https://docs.emelia.io/docs/linkedin-scraper/get-scraper.md) every 30 to 60 seconds, or register a [webhook](#webhooks). 4. When `status` is `done`: optionally **find the emails** ([Find the emails](https://docs.emelia.io/docs/linkedin-scraper/find-emails.md)), then **download the CSV** ([Download the CSV](https://docs.emelia.io/docs/linkedin-scraper/download-csv.md)), or use the Emelia list in `convertedListId`. ## Start an extraction [`POST /linkedin-scrappers`](https://docs.emelia.io/docs/linkedin-scraper/start-extraction.md) **curl** ```bash curl -X POST https://api.emelia.io/linkedin-scrappers \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "CTOs SaaS France", "authes": ["66e0f1a2b3c4d5e6f7a8b9c0"], "url": "https://www.linkedin.com/sales/search/people?query=..." }' # {"success":true,"scrapId":"66e0f1a2b3c4d5e6f7a8b9d0"} ``` **JavaScript** ```js const res = await fetch('https://api.emelia.io/linkedin-scrappers', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'CTOs SaaS France', authes: ['66e0f1a2b3c4d5e6f7a8b9c0'], url: 'https://www.linkedin.com/sales/search/people?query=...', }), }); const { scrapId } = await res.json(); ``` **Python** ```python import os import requests session = requests.Session() session.headers["Authorization"] = os.environ["EMELIA_API_KEY"] res = session.post("https://api.emelia.io/linkedin-scrappers", json={ "name": "CTOs SaaS France", "authes": ["66e0f1a2b3c4d5e6f7a8b9c0"], "url": "https://www.linkedin.com/sales/search/people?query=...", }) scrap_id = res.json()["scrapId"] ``` ### Accepted URLs and accounts | Search | URL | `authes` | | ------------------------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | Sales Navigator people search | `https://www.linkedin.com/sales/search/people...` | Accounts connected for scraping (the `authId` of [Connect an account](https://docs.emelia.io/docs/linkedin-scraper/connect-account.md)) | | Sales Navigator lead list | `https://www.linkedin.com/sales/lists/people/...` | Same | | Sales Navigator account search (companies) | `https://www.linkedin.com/sales/search/company...` | Same | | Classic LinkedIn people search | `https://www.linkedin.com/search/results/people/...` | LinkedIn accounts connected to your campaigns, set in the Emelia app | Every account must exist, belong to you and be valid. Several accounts share the chunks of a large extraction; otherwise one of them is picked. Another `https://linkedin.com/sales/...` URL is accepted at creation, then the scraper stops with `BAD_SALES_NAV_URL`. `plannedStart` schedules the start, at least one hour ahead. ### How many contacts * A Sales Navigator search returns 2,500 results at most (100 pages of 25). Lead lists and saved searches: 2,500. Searches filtered on your network: 1,000. Account searches: 1,000 companies. Classic searches: 1,000 people. * **Grow and Scale plans:** a search of 3,000 results or more that has filters is split automatically into chunks of 2,500 (`segmented: true`), extracted at 2,500 contacts per day and per LinkedIn account (5,000 with a Sales Navigator Advanced seat). Give several accounts to go faster. Above 80,000 results the scraper stops with `TOO_MUCH_RESULT`. * **Start plan:** capped at 2,500, no automatic split. * `max_contacts` stops the extraction early (on Sales Navigator, up to 25 more; ignored for split searches). ### Plans and credits Extraction is included in the Emelia subscriptions. Without a subscription, a workspace gets one free extraction, then extracts on credits: about 1 credit per 2 contacts, and the scraper pauses when the credits run out. ## Follow an extraction [`GET /linkedin-scrappers/{id}`](https://docs.emelia.io/docs/linkedin-scraper/get-scraper.md) **curl** ```bash curl https://api.emelia.io/linkedin-scrappers/66e0f1a2b3c4d5e6f7a8b9d0 \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const res = await fetch(`https://api.emelia.io/linkedin-scrappers/${scrapId}`, { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { scrap } = await res.json(); console.log(scrap.status, `${scrap.scrapIndex}/${scrap.totalContact}`, scrap.convertedListId); ``` **Python** ```python scrap = session.get(f"https://api.emelia.io/linkedin-scrappers/{scrap_id}").json()["scrap"] print(scrap["status"], scrap["scrapIndex"], "/", scrap["totalContact"], scrap.get("convertedListId")) ``` | `status` | Meaning | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `planified` | Waits for its `plannedStart`. | | `running` | Extracting. `scrapIndex` contacts out of `totalContact` (0 until LinkedIn gives the count). | | `paused` | Paused by you (split searches only) or out of credits. [Resume an extraction](https://docs.emelia.io/docs/linkedin-scraper/resume-extraction.md). | | `done` | Finished. The CSV is ready, and the Emelia list follows within a few minutes (`convertedListId`). | | `enriching` | Finding emails, then `done` again. | | `error` | Stopped. `error` gives the code: `INVALID_COOKIE`, `2FA_REQUIRED`, `BAD_SALES_NAV_URL`, `NEED_SUBSCRIPTION`, `TOO_MUCH_RESULT`, `RATE_LIMIT`, `SEGMENTATION_ERROR`, `PROCESS_ERROR`, `NO_AUTH_FOUND`. | After `INVALID_COOKIE` or `RATE_LIMIT`, refresh the account with [Refresh an account](https://docs.emelia.io/docs/linkedin-scraper/refresh-account.md), then resume the scraper. A Sales Navigator `RATE_LIMIT` is retried on its own about 12 hours later. The other codes are final: start a new extraction. ## Find the emails [`POST /linkedin-scrappers/{id}/enrich`](https://docs.emelia.io/docs/linkedin-scraper/find-emails.md) **curl** ```bash curl -X POST https://api.emelia.io/linkedin-scrappers/66e0f1a2b3c4d5e6f7a8b9d0/enrich \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{"country": "FR"}' ``` **JavaScript** ```js await fetch(`https://api.emelia.io/linkedin-scrappers/${scrapId}/enrich`, { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ country: 'FR' }), }); ``` **Python** ```python session.post(f"https://api.emelia.io/linkedin-scrappers/{scrap_id}/enrich", json={"country": "FR"}) ``` * Only on a finished people extraction that was never enriched. * You need at least as many credits as `totalContact`. That amount is taken at the start, and the credits of the contacts without email come back at the end: **1 credit per email found**. * An enrichment ends within about an hour. There is no webhook at the end: poll the scraper until `status` is `done` again; `enrichFound` gives the number of emails found. * To pay less, first [remove the contacts already extracted](https://docs.emelia.io/docs/linkedin-scraper/remove-known-contacts.md) by your 10 last scrapers. ## Download the CSV [`GET /linkedin-scrappers/{id}/download`](https://docs.emelia.io/docs/linkedin-scraper/download-csv.md) **curl** ```bash curl "https://api.emelia.io/linkedin-scrappers/66e0f1a2b3c4d5e6f7a8b9d0/download?format=3" \ -H "Authorization: $EMELIA_API_KEY" | jq -r .data > contacts.csv ``` **JavaScript** ```js import { writeFile } from 'node:fs/promises'; const res = await fetch(`https://api.emelia.io/linkedin-scrappers/${scrapId}/download?format=3`, { headers: { Authorization: process.env.EMELIA_API_KEY }, }); const { data } = await res.json(); // the CSV, as a string await writeFile('contacts.csv', data); ``` **Python** ```python csv = session.get( f"https://api.emelia.io/linkedin-scrappers/{scrap_id}/download", params={"format": 3}, ).json()["data"] open("contacts.csv", "w").write(csv) ``` The CSV comes as a string in `data`, with `;` as separator. `format` is `1` for the contacts with an email (after an enrichment), `2` for the contacts without, `3` for all. It works while the extraction runs: you get what was collected so far. After an enrichment, the email is in `email_emelia` and its verdict in `qualification_emelia`. ## Webhooks [`POST /linkedin-scrappers/{id}/webhook`](https://docs.emelia.io/docs/linkedin-scraper/add-webhook.md) Register a URL right after starting the extraction, with `{"webhookUrl": "https://hooks.yourcompany.com/emelia/scraper", "events": ["FINISHED", "ERROR"]}`. Emelia then sends `{ "event", "id", "name", "error" }`: * `FINISHED` when the extraction ends (every chunk included); * `ERROR` when it stops on an error code, at most once per hour for the same code. Every registered URL receives every event, whatever its `events`. One attempt only, no signature: check the scraper with `GET /linkedin-scrappers/{id}` when you receive a call. The success response of this route is an empty object `{}`. ## Scraping accounts [`POST /linkedin-scrappers/authes`](https://docs.emelia.io/docs/linkedin-scraper/connect-account.md) A scraping account is a LinkedIn session, sent as the cookies of a browser where the user is logged in to LinkedIn and Sales Navigator. `li_at` is required; `li_a`, `ua`, `additionnalsCookie` (objects `{ name, value, domain }`), `viewport` and `country` are strongly recommended. The response gives the `authId` to use in `authes`. > **Session cookies are secrets.** These cookies open the LinkedIn account. Send them only to Emelia, never log them, and protect your API key accordingly. There is no API route to remove a scraping account yet. When a session expires, update the same account with [Refresh an account](https://docs.emelia.io/docs/linkedin-scraper/refresh-account.md) rather than creating a new one, and send the complete set of cookies every time: omitted fields are cleared. --- # Create a LinkedIn scraper (start an extraction) Source: https://docs.emelia.io/docs/linkedin-scraper/start-extraction Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `POST https://api.emelia.io/linkedin-scrappers` Starts extracting the people (or companies) of a LinkedIn search into a CSV. The extraction runs in the background on Emelia servers: this call only creates the scraper and returns its id (`scrapId`). Follow it with `GET /linkedin-scrappers/{id}` or with a webhook (`POST /linkedin-scrappers/{id}/webhook`). **Accepted URLs and accounts** - Sales Navigator people search (`https://www.linkedin.com/sales/search/people...`), lead list (`.../sales/lists/people/...`) or account search (`.../sales/search/company...`, companies instead of people). `authes` must contain ids of LinkedIn accounts connected for scraping (the `authId` returned by `POST /linkedin-scrappers/authes`). - Classic LinkedIn people search (`https://www.linkedin.com/search/results/people/...`). It runs on a LinkedIn account connected for campaigns: `authes` must contain ids of LinkedIn accounts connected for campaigns, not of accounts connected for scraping. - Any other `https://linkedin.com/sales/...` URL is accepted here but the scraper then stops with the error `BAD_SALES_NAV_URL`. **Volume limits** (applied during the extraction, not at creation) - Sales Navigator never returns more than 2,500 results per search (100 pages of 25). Lead lists and saved searches are capped at 2,500, searches filtered on your network (`CONNECTION_OF`) at 1,000, account searches at 1,000 companies, classic searches at 1,000 people. - Grow and Scale plans: a search with 3,000 results or more that contains filters is split automatically into chunks of at most 2,500 (`segmented: true`) and extracted over several days, at 2,500 contacts per day per LinkedIn account (5,000 for an account with a Sales Navigator Advanced seat). Give several accounts in `authes` to go faster. Above 80,000 results the scraper stops with `TOO_MUCH_RESULT`. - Start plan and free use: capped at 2,500, no automatic split. - `max_contacts` stops the extraction early. On Sales Navigator it is applied per page of 25 (you can get up to 25 more) and it is ignored for segmented searches. On classic searches it is exact. **Rules checked by this call**: the name is unique among your scrapers; with a single account and no `plannedStart`, that account must not already run a scraper; every account exists, belongs to you and is valid; `plannedStart` is at least one hour ahead; your workspace has an active subscription, or has not used its free extraction yet, or still has credits (without subscription, extraction costs about 1 credit per 2 contacts and pauses when credits run out). Fields not listed in the schema are rejected. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | Unique among your scrapers. Keep it at 100 characters or less (the rename limit). Min length 1. | | `authes` | array of string | yes | LinkedIn account ids. Sales Navigator URL: ids of accounts connected for scraping (`authId` returned by `POST /linkedin-scrappers/authes`). Classic search URL: ids of the LinkedIn accounts connected to your campaigns. All must be valid and yours. Several accounts share the chunks of a segmented extraction; otherwise one of them is picked at random. | | `url` | string (uri) | yes | Sales Navigator people search, lead list or account search URL, or classic LinkedIn people search URL, copied from the browser address bar. | | `plannedStart` | string (date-time) | no | Optional ISO 8601 start date, at least one hour ahead. The scraper stays `planified` until then. | | `max_contacts` | integer | no | Optional maximum number of contacts. Approximate on Sales Navigator (page of 25), ignored on segmented extractions, exact on classic searches. Range: 1 to .... | ```json { "name": "string", "authes": [ "string" ], "url": "string", "plannedStart": "2026-10-01T09:00:00.000Z", "max_contacts": 1 } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin-scrappers" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "string", "authes": [ "string" ], "url": "string", "plannedStart": "2026-10-01T09:00:00.000Z", "max_contacts": 1 }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "string", "authes": [ "string" ], "url": "string", "plannedStart": "2026-10-01T09:00:00.000Z", "max_contacts": 1 }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin-scrappers", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "string", "authes": [ "string" ], "url": "string", "plannedStart": "2026-10-01T09:00:00.000Z", "max_contacts": 1 }, ) print(response.json()) ``` ## Responses ### 200: Scrapper created. `status` is `running` (or `planified` with `plannedStart`). | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `scrapId` | string | yes | Id of the new scraper. | ```json { "success": true, "scrapId": "66f1a2b3c4d5e6f7a8b9c0d1" } ``` ### 400: Invalid request or rule not met. Validation errors carry `code: INVALID_REQUEST`; business errors only have `error`. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # List your LinkedIn scrapers Source: https://docs.emelia.io/docs/linkedin-scraper/list-scrapers Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `GET https://api.emelia.io/linkedin-scrappers` Returns the scrapers of your workspace, newest first, 20 per page. Deleted scrapers are not returned. Use it to show a history or to find a scraper id. To follow one extraction, poll `GET /linkedin-scrappers/{id}` instead: it is lighter and returns the same object. For running segmented scrapers (large searches split into chunks, Grow and Scale plans), two computed fields are added: `nextRunningSegment` (when the next chunk is scheduled) and `betweenSegment` (true when no chunk is being extracted right now). Pitfall: each object also contains internal fields not described here (cursors, legacy ids). Do not rely on them. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `page` | integer | no | Page number, starting at 0. 20 scrapers per page. Missing, non numeric or negative values mean 0. An empty `scraps` array means you went past the last page. Default: `0`. Range: 0 to .... | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/linkedin-scrappers" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers', { 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/linkedin-scrappers", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: One page of scrapers. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `scraps` | array of object | yes | | | `scraps[]._id` | string | no | Scrapper id. | | `scraps[].userId` | string | no | Workspace id. Only returned by `GET /linkedin-scrappers/{id}`. | | `scraps[].name` | string | no | | | `scraps[].url` | string | no | Search URL given at creation. | | `scraps[].scrapType` | enum: `people`, `account`, `classic` | no | `people`: Sales Navigator people search or lead list. `account`: Sales Navigator account (company) search; detected when the extraction starts, so it reads `people` before. `classic`: classic LinkedIn people search. | | `scraps[].status` | enum: `running`, `planified`, `paused`, `enriching`, `done`, `error` | no | Life cycle of a scraper: - `planified`: waiting for `plannedStart`. - `running`: extraction in progress. For a segmented extraction, also between two chunks (see `betweenSegment`). - `paused`: paused by you (segmented extractions only) or by Emelia when credits ran out. Restart with resume. - `done`: extraction finished; also the state after an enrichment ends. - `enriching`: email enrichment in progress (`POST /{id}/enrich`). - `error`: stopped, the reason is in `error`. | | `scraps[].error` | enum: `INVALID_COOKIE`, `2FA_REQUIRED`, `BAD_SALES_NAV_URL`, `NEED_SUBSCRIPTION`, `TOO_MUCH_RESULT`, `RATE_LIMIT`, `SEGMENTATION_ERROR`, `PROCESS_ERROR`, `NO_AUTH_FOUND` or null | no | Error code when `status` is `error`. | | `scraps[].errorDetail` | object | no | Internal diagnostic written by the scraper (free text in French). Not a stable contract: use `error`. | | `scraps[].totalContact` | integer | no | Contacts to extract: 0 until LinkedIn reports the result count, then capped by the plan rules and `maxContacts`. Final count once `done`. Reduced by a split or a filter. | | `scraps[].scrapIndex` | integer | no | Contacts extracted so far. | | `scraps[].maxContacts` | integer | no | Value of `max_contacts` at creation, 0 = no limit. | | `scraps[].segmented` | boolean | no | True when the search was split into chunks (Grow and Scale, 3,000 results or more). | | `scraps[].segments` | array of object | no | | | `scraps[].estimatedEnd` | string (date-time) | no | Estimated end of a segmented extraction. | | `scraps[].nextRunningSegment` | string or null (date-time) | no | Computed for running segmented scrapers: when the next chunk is scheduled. | | `scraps[].betweenSegment` | boolean | no | Computed for running segmented scrapers: true when no chunk is being extracted right now. | | `scraps[].plannedStart` | string (date-time) | no | Scheduled start, when given at creation. | | `scraps[].date` | string (date-time) | no | Creation date. | | `scraps[].lastUpdate` | string (date-time) | no | Last time the scraper wrote progress. | | `scraps[].enrichEnabled` | boolean | no | True once an email enrichment was started. | | `scraps[].startEnrichDate` | string (date-time) | no | Start of the email enrichment. | | `scraps[].enrichIndex` | integer | no | Contacts processed by the enrichment. Only set at the end. | | `scraps[].enrichFound` | integer | no | Emails found. Only set at the end of the enrichment. | | `scraps[].enrichPhoneEnabled` | boolean | no | True when a phone enrichment was started from the app. | | `scraps[].enrichPhoneIndex` | integer | no | | | `scraps[].enrichPhoneFound` | integer | no | | | `scraps[].processing` | boolean | no | True while `POST /{id}/filter` runs. | | `scraps[].webhooks` | array of object | no | | | `scraps[].authes` | array of string | no | LinkedIn account ids used by the scraper. | | `scraps[].auth` | string | no | Legacy single account id (old scrapers only). | | `scraps[].convertedListId` | string or null | no | Id of the Emelia list created automatically from the finished scraper, `null` before. | ```json { "success": true, "scraps": [ { "_id": "66f1a2b3c4d5e6f7a8b9c0d1", "authes": [ "66e0f1a2b3c4d5e6f7a8b9c0", "66e0f1a2b3c4d5e6f7a8b9c1" ], "name": "Acme CTOs France", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))))", "scrapType": "people", "status": "running", "totalContact": 7400, "scrapIndex": 5000, "maxContacts": 0, "segmented": true, "segments": [ { "_id": "66f1a2b3c4d5e6f7a8b9c101", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))%2C(type%3ACOMPANY_HEADCOUNT%2Cvalues%3AList((id%3AB%2Ctext%3A1-10%2CselectionType%3AINCLUDED)))))", "scrapIndex": 2500, "count": 2500, "complete": true }, { "_id": "66f1a2b3c4d5e6f7a8b9c102", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))%2C(type%3ACOMPANY_HEADCOUNT%2Cvalues%3AList((id%3AC%2Ctext%3A11-50%2CselectionType%3AINCLUDED)))))", "scrapIndex": 2500, "count": 2500, "complete": true }, { "_id": "66f1a2b3c4d5e6f7a8b9c103", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))%2C(type%3ACOMPANY_HEADCOUNT%2Cvalues%3AList((id%3AD%2Ctext%3A51-200%2CselectionType%3AINCLUDED)))))", "scrapIndex": 0, "count": 2400 } ], "estimatedEnd": "2026-10-12T13:40:00.000Z", "nextRunningSegment": "2026-10-12T09:11:00.000Z", "betweenSegment": true, "date": "2026-10-11T08:02:41.512Z", "lastUpdate": "2026-10-11T11:14:03.220Z", "enrichEnabled": false, "enrichIndex": 0, "enrichFound": 0, "enrichPhoneEnabled": false, "enrichPhoneIndex": 0, "enrichPhoneFound": 0, "webhooks": [ { "_id": "66f1a2b3c4d5e6f7a8b9c0f1", "webhookUrl": "https://hooks.acme.example/emelia/scrapper", "events": [ "FINISHED", "ERROR" ] } ], "convertedListId": null }, { "_id": "66fd0e1f2a3b4c5d6e7f8a90", "authes": [ "66e0f1a2b3c4d5e6f7a8b9c0" ], "name": "Acme marketing leads", "url": "https://www.linkedin.com/sales/lists/people/7212345678901234567?sortCriteria=CREATED_TIME&sortOrder=DESCENDING", "scrapType": "people", "status": "done", "totalContact": 1840, "scrapIndex": 1840, "maxContacts": 0, "segmented": false, "segments": [], "date": "2026-10-02T14:05:09.000Z", "lastUpdate": "2026-10-02T14:41:55.000Z", "enrichEnabled": true, "startEnrichDate": "2026-10-02T15:20:11.000Z", "enrichIndex": 1840, "enrichFound": 1022, "enrichPhoneEnabled": false, "enrichPhoneIndex": 0, "enrichPhoneFound": 0, "webhooks": [], "convertedListId": "66f2b3c4d5e6f7a8b9c0d1e2" } ] } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Get one LinkedIn scraper (poll its progress) Source: https://docs.emelia.io/docs/linkedin-scraper/get-scraper Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `GET https://api.emelia.io/linkedin-scrappers/{id}` Returns one scraper with its progress. This is the call to poll while an extraction or an enrichment runs (every 30 to 60 seconds is enough). **Reading the progress** - `status`: see `ScrapperStatus`. The extraction is over when `status` is `done`; `error` gives the reason when `status` is `error`. - `scrapIndex` contacts extracted out of `totalContact` (0 until LinkedIn reports the result count, then capped by the plan rules). - Segmented searches: `segments`, `estimatedEnd`, `nextRunningSegment`, `betweenSegment`. - After `POST /{id}/enrich`: `status` is `enriching`, then `done` again. `enrichIndex` and `enrichFound` are only filled at the end of the enrichment. - `convertedListId`: id of the Emelia list automatically created from a finished scraper (usually within a few minutes after `done`), `null` before. Unlike the list route, this response also contains `userId`. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1', { 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/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: The scraper. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `scrap` | object | yes | A scraper (one extraction of a LinkedIn search). | | `scrap._id` | string | no | Scrapper id. | | `scrap.userId` | string | no | Workspace id. Only returned by `GET /linkedin-scrappers/{id}`. | | `scrap.name` | string | no | | | `scrap.url` | string | no | Search URL given at creation. | | `scrap.scrapType` | enum: `people`, `account`, `classic` | no | `people`: Sales Navigator people search or lead list. `account`: Sales Navigator account (company) search; detected when the extraction starts, so it reads `people` before. `classic`: classic LinkedIn people search. | | `scrap.status` | enum: `running`, `planified`, `paused`, `enriching`, `done`, `error` | no | Life cycle of a scraper: - `planified`: waiting for `plannedStart`. - `running`: extraction in progress. For a segmented extraction, also between two chunks (see `betweenSegment`). - `paused`: paused by you (segmented extractions only) or by Emelia when credits ran out. Restart with resume. - `done`: extraction finished; also the state after an enrichment ends. - `enriching`: email enrichment in progress (`POST /{id}/enrich`). - `error`: stopped, the reason is in `error`. | | `scrap.error` | enum: `INVALID_COOKIE`, `2FA_REQUIRED`, `BAD_SALES_NAV_URL`, `NEED_SUBSCRIPTION`, `TOO_MUCH_RESULT`, `RATE_LIMIT`, `SEGMENTATION_ERROR`, `PROCESS_ERROR`, `NO_AUTH_FOUND` or null | no | Error code when `status` is `error`. | | `scrap.errorDetail` | object | no | Internal diagnostic written by the scraper (free text in French). Not a stable contract: use `error`. | | `scrap.totalContact` | integer | no | Contacts to extract: 0 until LinkedIn reports the result count, then capped by the plan rules and `maxContacts`. Final count once `done`. Reduced by a split or a filter. | | `scrap.scrapIndex` | integer | no | Contacts extracted so far. | | `scrap.maxContacts` | integer | no | Value of `max_contacts` at creation, 0 = no limit. | | `scrap.segmented` | boolean | no | True when the search was split into chunks (Grow and Scale, 3,000 results or more). | | `scrap.segments` | array of object | no | | | `scrap.estimatedEnd` | string (date-time) | no | Estimated end of a segmented extraction. | | `scrap.nextRunningSegment` | string or null (date-time) | no | Computed for running segmented scrapers: when the next chunk is scheduled. | | `scrap.betweenSegment` | boolean | no | Computed for running segmented scrapers: true when no chunk is being extracted right now. | | `scrap.plannedStart` | string (date-time) | no | Scheduled start, when given at creation. | | `scrap.date` | string (date-time) | no | Creation date. | | `scrap.lastUpdate` | string (date-time) | no | Last time the scraper wrote progress. | | `scrap.enrichEnabled` | boolean | no | True once an email enrichment was started. | | `scrap.startEnrichDate` | string (date-time) | no | Start of the email enrichment. | | `scrap.enrichIndex` | integer | no | Contacts processed by the enrichment. Only set at the end. | | `scrap.enrichFound` | integer | no | Emails found. Only set at the end of the enrichment. | | `scrap.enrichPhoneEnabled` | boolean | no | True when a phone enrichment was started from the app. | | `scrap.enrichPhoneIndex` | integer | no | | | `scrap.enrichPhoneFound` | integer | no | | | `scrap.processing` | boolean | no | True while `POST /{id}/filter` runs. | | `scrap.webhooks` | array of object | no | | | `scrap.authes` | array of string | no | LinkedIn account ids used by the scraper. | | `scrap.auth` | string | no | Legacy single account id (old scrapers only). | | `scrap.convertedListId` | string or null | no | Id of the Emelia list created automatically from the finished scraper, `null` before. | ```json { "success": true, "scrap": { "_id": "66f1a2b3c4d5e6f7a8b9c0d1", "userId": "65a0b1c2d3e4f5a6b7c8d9e0", "authes": [ "66e0f1a2b3c4d5e6f7a8b9c0", "66e0f1a2b3c4d5e6f7a8b9c1" ], "name": "Acme CTOs France", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))))", "scrapType": "people", "status": "running", "totalContact": 7400, "scrapIndex": 5000, "maxContacts": 0, "segmented": true, "segments": [ { "_id": "66f1a2b3c4d5e6f7a8b9c101", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))%2C(type%3ACOMPANY_HEADCOUNT%2Cvalues%3AList((id%3AB%2Ctext%3A1-10%2CselectionType%3AINCLUDED)))))", "scrapIndex": 2500, "count": 2500, "complete": true }, { "_id": "66f1a2b3c4d5e6f7a8b9c102", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))%2C(type%3ACOMPANY_HEADCOUNT%2Cvalues%3AList((id%3AC%2Ctext%3A11-50%2CselectionType%3AINCLUDED)))))", "scrapIndex": 2500, "count": 2500, "complete": true }, { "_id": "66f1a2b3c4d5e6f7a8b9c103", "url": "https://www.linkedin.com/sales/search/people?query=(filters%3AList((type%3ACURRENT_TITLE%2Cvalues%3AList((text%3ACTO%2CselectionType%3AINCLUDED)))%2C(type%3AREGION%2Cvalues%3AList((text%3AFrance%2CselectionType%3AINCLUDED)))%2C(type%3ACOMPANY_HEADCOUNT%2Cvalues%3AList((id%3AD%2Ctext%3A51-200%2CselectionType%3AINCLUDED)))))", "scrapIndex": 0, "count": 2400 } ], "estimatedEnd": "2026-10-12T13:40:00.000Z", "nextRunningSegment": "2026-10-12T09:11:00.000Z", "betweenSegment": true, "date": "2026-10-11T08:02:41.512Z", "lastUpdate": "2026-10-11T11:14:03.220Z", "enrichEnabled": false, "enrichIndex": 0, "enrichFound": 0, "enrichPhoneEnabled": false, "enrichPhoneIndex": 0, "enrichPhoneFound": 0, "webhooks": [ { "_id": "66f1a2b3c4d5e6f7a8b9c0f1", "webhookUrl": "https://hooks.acme.example/emelia/scrapper", "events": [ "FINISHED", "ERROR" ] } ], "convertedListId": null } } ``` ### 400: Unknown or deleted scraper. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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" } ``` ### 404: The id is not 24 hexadecimal characters: no route matches and the server answers with its default HTML page (not JSON). Content type: `text/html`. ### 500: Unexpected server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Download the contacts of a scraper (CSV) Source: https://docs.emelia.io/docs/linkedin-scraper/download-csv Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `GET https://api.emelia.io/linkedin-scrappers/{id}/download` Returns the CSV of a scraper as a string in `data` (a JSON response, not a file). Separator `;`, first line = column names, UTF-8 without BOM, values quoted only when needed. Large extractions give large responses (several MB). It can be called while the extraction runs: you get what has been collected so far. `format`: `1` = only contacts with an email (requires a finished enrichment with at least one email), `2` = only contacts without an email (all contacts if the scraper was never enriched), `3` = all contacts. **People columns** (Sales Navigator and classic searches): id, firstName, lastName, fullName, location, isPremium, openProfile, linkedinUrlProfile, summary, title, company, companyId, companyLocation, companyDescription, companySize, estimatedMinRevenue, estimatedMaxRevenue, industry, description, companyPage, companyWebsite, type. `id` is the Sales Navigator profile key and can contain commas. Fields that LinkedIn does not provide stay empty (classic searches fill fewer columns). **After an email enrichment** the file is rewritten in this order: id, email_emelia, qualification_emelia, gender_emelia, firstName, lastName, fullName, location, isPremium, openProfile, linkedinUrlProfile, phone_number_emelia, phone_qualification_emelia, phone_country_emelia, summary, title, company, companyId, companyLocation, companyDescription, companySize, estimatedMaxRevenue, estimatedMinRevenue, industry, description, companyPage, companyWebsite, domain_emelia, type. **Account search columns** (companies): id, companyName, companyPage, companyDescription, companySize, employeeCountRange, industry, companyWebsite, companyLocation, yearFounded, type, estimatedMinRevenue, estimatedMaxRevenue. Once a scraper is `done`, its contacts are also imported automatically into an Emelia list (`convertedListId`), that you can manage with the Lists endpoints. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Query parameters | Name | Type | Required | Description | |---|---|---|---| | `format` | enum: `1`, `2`, `3` | yes | 1 = contacts with an email only, 2 = contacts without an email only, 3 = all contacts. | ## Example request **cURL** ```bash curl -X GET "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/download?format=3" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/download?format=3', { 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/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/download?format=3", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: CSV content in `data`. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `data` | string | yes | CSV content: `;` separator, header line first, UTF-8 without BOM. | ```json { "success": true, "data": "string" } ``` ### 400: Invalid format, unknown scraper or nothing to export. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Find the emails of a finished scraper Source: https://docs.emelia.io/docs/linkedin-scraper/find-emails Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `POST https://api.emelia.io/linkedin-scrappers/{id}/enrich` Looks for the professional email of every contact of a finished people extraction. Asynchronous: `status` becomes `enriching`, then `done` again when the emails are written into the CSV. An enrichment is closed after about one hour at most, with what was found by then. **Credits**: you need at least as many credits as `totalContact`. That amount is taken when the enrichment starts, then the credits of the contacts without an email are given back at the end: you pay 1 credit per email found. Your balance therefore drops temporarily during the enrichment. `country` is an ISO 3166-1 alpha-2 code (for example `FR`) used as a hint for the search. It is not validated. Conditions: `status` is `done` and the scraper was never enriched (`enrichEnabled` false). To enrich the first part of a long segmented extraction, split it first (`POST /linkedin-scrappers/{id}/split`). Only for people searches: an account (company) search has no person to look up. After completion: `enrichFound` is the number of emails found. Download with `format=1` to get only the contacts with an email, and read the columns `email_emelia`, `qualification_emelia`, `gender_emelia`, `domain_emelia`. No webhook is sent at the end of an enrichment: poll `GET /linkedin-scrappers/{id}`. Phone number enrichment of a scraper is not available in this API. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `country` | string | yes | ISO 3166-1 alpha-2 country code of the contacts, used as a hint (for example `FR`, `US`). Not validated. Min length 1. | ```json { "country": "FR" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/enrich" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "country": "FR" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/enrich', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "country": "FR" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/enrich", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "country": "FR" }, ) print(response.json()) ``` ## Responses ### 200: Enrichment started (`status` is now `enriching`). `status` in this response is always the boolean `true`. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `status` | boolean | yes | Always `true` (not a status string). | ```json { "success": true, "status": true } ``` ### 400: The enrichment cannot start. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Remove contacts already extracted by your other scrapers Source: https://docs.emelia.io/docs/linkedin-scraper/remove-known-contacts Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `POST https://api.emelia.io/linkedin-scrappers/{id}/filter` Removes from this scraper the LinkedIn profiles that already appear in your 10 most recent finished scrapers (comparison on the profile `id` column). Run it on a finished scraper before enriching it: fewer contacts, fewer credits. `filters` is a bit field. Only `1` exists today (remove profiles found in your other scrapers); use `1`. Asynchronous: the call returns at once and sets `processing` to true on the scraper. When the filter ends, `processing` is false again and `totalContact` and `scrapIndex` give the new count. Large files can take several minutes. Pitfalls: the CSV is rewritten and the previous version cannot be restored through the API. The filter is not restricted to finished scrapers: do not run it while an extraction is in progress. `filters: 0` is accepted but nothing runs and `processing` stays true. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `filters` | integer | yes | Bit field. `1` = remove the profiles found in your 10 most recent finished scrapers. No other bit is implemented. Range: 1 to .... | ```json { "filters": 1 } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/filter" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filters": 1 }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/filter', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "filters": 1 }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/filter", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "filters": 1 }, ) print(response.json()) ``` ## Responses ### 200: Filter started. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `data` | boolean | yes | Always `true`: the filter was started. | ```json { "success": true, "data": true } ``` ### 400: Invalid body or unknown scraper. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Pause a segmented extraction Source: https://docs.emelia.io/docs/linkedin-scraper/pause-extraction Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `PUT https://api.emelia.io/linkedin-scrappers/{id}/pause` Pauses a large extraction split into chunks (`segmented: true`, Grow and Scale plans). Only allowed while `status` is `running`. Other scrapers cannot be paused. The pause is not immediate: the chunk being extracted goes to its end, only the next chunks are unscheduled. `status` becomes `paused`. Restart with `PUT /linkedin-scrappers/{id}/resume`. A scraper can also be paused by Emelia when it runs on credits (no subscription) and the credits run out. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Example request **cURL** ```bash curl -X PUT "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/pause" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/pause', { method: 'PUT', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.put( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/pause", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Paused. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: The scraper cannot be paused. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Resume a paused, scheduled or failed scraper Source: https://docs.emelia.io/docs/linkedin-scraper/resume-extraction Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `PUT https://api.emelia.io/linkedin-scrappers/{id}/resume` Restarts a scraper that is: - `paused` (by you, or automatically when credits ran out); - `planified`: it starts now instead of at `plannedStart`; - in `error` with `INVALID_COOKIE` or `RATE_LIMIT`, once its LinkedIn account is valid again (refresh it first with `PUT /linkedin-scrappers/authes/{id}`, or reconnect the campaign account for a classic search). Other error codes are final: create a new scraper. A segmented extraction restarts with its remaining chunks. For Sales Navigator, a `RATE_LIMIT` stop is retried automatically about 12 hours later. Checks: an active subscription or remaining credits; no other running scraper on the same account. Known issues: the running scraper check can refuse the resume with `You already have a running scrap` as soon as any other scraper of the workspace is running, even on another account. An unknown id is reported as `This scrap is in invalid state` (or as the running scraper message), not as not found. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Example request **cURL** ```bash curl -X PUT "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/resume" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/resume', { method: 'PUT', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.put( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/resume", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Restarted (`status` is `running`). | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: The scraper cannot be resumed. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Detach the part already extracted from a segmented extraction Source: https://docs.emelia.io/docs/linkedin-scraper/split-extraction Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `POST https://api.emelia.io/linkedin-scrappers/{id}/split` Lets you use now what a long segmented extraction has already collected, while the rest goes on. Effect: a new scraper named ` 1` is created with `status` `done` and the contacts extracted so far; its id is returned in `scrapId`. The original scraper keeps its id, is renamed ` 2`, keeps only the chunks not finished yet and restarts its counter (`scrapIndex` back to 0, `totalContact` reduced accordingly). The new part is then imported automatically into an Emelia list, like any finished scraper. Typical use: enrich or download the first contacts of an extraction that will take days (enrichment requires `status` `done`). Conditions: `segmented` is true, `status` is neither `enriching` nor `done`, at least one contact was extracted, and nothing was written during the last 10 minutes (otherwise retry later). Not to be confused with the automatic split of large searches into chunks, which Emelia does by itself on Grow and Scale plans. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/split" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/split', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/split", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Split done. `scrapId` is the NEW finished scraper. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `scrapId` | string | yes | Id of the NEW finished scraper (` 1`) holding the contacts extracted so far. The original id keeps running as ` 2`. | ```json { "success": true, "scrapId": "66f1a2b3c4d5e6f7a8b9c0d2" } ``` ### 400: The scraper cannot be split. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Rename a LinkedIn scraper Source: https://docs.emelia.io/docs/linkedin-scraper/rename-scraper Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `PUT https://api.emelia.io/linkedin-scrappers/{id}/rename` Changes the name of a scraper. 1 to 100 characters, unique among your scrapers. Pitfall: renaming a scraper to its current name is refused (`A scrap with this name already exists`). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | Min length 1. | ```json { "name": "Acme CTOs France Q4" } ``` ## Example request **cURL** ```bash curl -X PUT "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/rename" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Acme CTOs France Q4" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/rename', { method: 'PUT', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "name": "Acme CTOs France Q4" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.put( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/rename", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "name": "Acme CTOs France Q4" }, ) print(response.json()) ``` ## Responses ### 200: Renamed. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Invalid name or unknown scraper. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Delete a LinkedIn scraper Source: https://docs.emelia.io/docs/linkedin-scraper/delete-scraper Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `DELETE https://api.emelia.io/linkedin-scrappers/{id}` Deletes a scraper. It disappears from the API and from the app. A Sales Navigator extraction in progress is stopped and the next chunks of a segmented extraction do not start. Pitfalls: the deletion is logical (Emelia keeps the extracted file). A classic LinkedIn search that is already extracting is not stopped: it runs to its end (and still sends its FINISHED webhook). The Emelia list created from the scraper (`convertedListId`) is not deleted. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1" \ -H "Authorization: $EMELIA_API_KEY" ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY }, }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, ) print(response.json()) ``` ## Responses ### 200: Deleted. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | ```json { "success": true } ``` ### 400: Unknown scraper or malformed id. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Register a webhook on a scraper Source: https://docs.emelia.io/docs/linkedin-scraper/add-webhook Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `POST https://api.emelia.io/linkedin-scrappers/{id}/webhook` Registers a URL that Emelia calls when this scraper finishes or stops on an error. Register it right after creating the scraper. Several URLs can be registered on the same scraper; the same URL cannot be added twice. **What is sent**: a `POST` with a JSON body `{ "event", "id", "name", "error" }` (see `ScrapperWebhookEvent`). `FINISHED` is sent when the extraction ends (`status` `done`, all chunks included). `ERROR` is sent when the scraper stops with an error code, at most once per hour for the same scraper and code. **Current behavior to know**: every registered URL receives every event, whatever its `events` list; `ERROR` is only sent if at least one registered URL asked for `ERROR`. One attempt only: no retry, no signature, no custom header, so check the scraper with `GET /linkedin-scrappers/{id}` when you receive a call. Nothing is sent at the end of an enrichment. `events` must contain `FINISHED` or `ERROR`; other values are accepted and ignored. Known issue: the success response is currently an empty JSON object `{}` (no `success` field). Treat HTTP 200 as success. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `webhookUrl` | string (uri) | yes | Public URL that accepts a POST with a JSON body. | | `events` | array of enum: `FINISHED`, `ERROR` | yes | At least one of `FINISHED`, `ERROR`. Today every registered URL receives every event anyway. | ```json { "webhookUrl": "https://hooks.acme.example/emelia/scrapper", "events": [ "FINISHED", "ERROR" ] } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/webhook" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://hooks.acme.example/emelia/scrapper", "events": [ "FINISHED", "ERROR" ] }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/webhook', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "webhookUrl": "https://hooks.acme.example/emelia/scrapper", "events": [ "FINISHED", "ERROR" ] }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/webhook", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "webhookUrl": "https://hooks.acme.example/emelia/scrapper", "events": [ "FINISHED", "ERROR" ] }, ) print(response.json()) ``` ## Responses ### 200: Registered. Current body: `{}`. ```json {} ``` ### 400: Invalid webhook or unknown scraper. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Remove a webhook from a scraper Source: https://docs.emelia.io/docs/linkedin-scraper/remove-webhook Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `DELETE https://api.emelia.io/linkedin-scrappers/{id}/webhook` Unregisters a webhook URL (exact match). The URL goes in the JSON body of the `DELETE` request: make sure your HTTP client sends a body with `DELETE`. Removing a URL that is not registered is not an error. Known issue: the success response is currently an empty JSON object `{}`. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Scrapper id (`_id` returned by `POST /linkedin-scrappers` or `GET /linkedin-scrappers`). 24 hexadecimal characters. | ## Request body Content type: `application/json`, required. | Field | Type | Required | Description | |---|---|---|---| | `webhookUrl` | string (uri) | yes | URL to remove (exact match). | ```json { "webhookUrl": "https://hooks.acme.example/emelia/scrapper" } ``` ## Example request **cURL** ```bash curl -X DELETE "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/webhook" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhookUrl": "https://hooks.acme.example/emelia/scrapper" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/webhook', { method: 'DELETE', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "webhookUrl": "https://hooks.acme.example/emelia/scrapper" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.delete( "https://api.emelia.io/linkedin-scrappers/66f1a2b3c4d5e6f7a8b9c0d1/webhook", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "webhookUrl": "https://hooks.acme.example/emelia/scrapper" }, ) print(response.json()) ``` ## Responses ### 200: Removed (or was not registered). Current body: `{}`. ```json {} ``` ### 400: Invalid body or unknown scraper. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Connect a LinkedIn account for scraping (session cookies) Source: https://docs.emelia.io/docs/linkedin-scraper/connect-account Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `POST https://api.emelia.io/linkedin-scrappers/authes` Connects a LinkedIn account for Sales Navigator scraping by sending the cookies of a browser session where the user is logged in to LinkedIn and Sales Navigator. No password is involved. The Emelia browser extension does this for you; use this route to automate it. `li_at` is required. Strongly recommended: `li_a` (Sales Navigator session), `ua` (user agent of the browser the cookies come from), `additionnalsCookie` (the other linkedin.com cookies, as objects `{ name, value, domain }`), `viewport` (that browser window size) and `country` (ISO alpha-2 code: Emelia browses LinkedIn through a proxy located in this country; default the workspace country, then FR). Returns `authId`, to use in `authes` of `POST /linkedin-scrappers`. The same `li_at` cannot be registered twice. When a session expires, update the existing account with `PUT /linkedin-scrappers/authes/{id}` instead of creating a new one. Emelia stores these cookies and refreshes them after each scraping session. There is currently no API route to remove a connected scraping account. Pitfalls: `jsessionid` is accepted but ignored (send `JSESSIONID` inside `additionnalsCookie`). Cookies sent as `"name=value"` strings are accepted but not used by the Sales Navigator scraper: send objects. Fields not described here are reserved for Emelia apps; any other field is rejected. ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Request body Content type: `application/json`, required. Session cookies of a LinkedIn account logged in to Sales Navigator. | Field | Type | Required | Description | |---|---|---|---| | `li_at` | string | yes | Value of the `li_at` cookie (LinkedIn session). Required. Min length 1. | | `li_a` | string | no | Value of the `li_a` cookie (Sales Navigator session). | | `jsessionid` | string | no | Accepted but ignored. Send `JSESSIONID` in `additionnalsCookie`. | | `ua` | string | no | User agent of the browser the cookies come from. | | `additionnalsCookie` | array of object or string | no | Other linkedin.com cookies of the same session (spelling with two n is the real field name). `li_at`, `li_a` and `JSESSIONID` entries are dropped (the latter is then lost). | | `viewport` | object | no | Window size of the browser the cookies come from. Both values required when the object is sent. | | `viewport.width` | number | yes | | | `viewport.height` | number | yes | | | `country` | string | no | ISO 3166-1 alpha-2 code of the country the account is used from (for example `FR`). Emelia browses LinkedIn through a proxy located in this country. | ```json { "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" } ``` ## Example request **cURL** ```bash curl -X POST "https://api.emelia.io/linkedin-scrappers/authes" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/authes', { method: 'POST', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.post( "https://api.emelia.io/linkedin-scrappers/authes", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" }, ) print(response.json()) ``` ## Responses ### 200: Account connected. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `authId` | string | yes | Account id. | ```json { "success": true, "authId": "66e0f1a2b3c4d5e6f7a8b9c0" } ``` ### 400: Invalid body or cookie already registered. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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 server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # Refresh the cookies of a connected scraping account Source: https://docs.emelia.io/docs/linkedin-scraper/refresh-account Guide: [LinkedIn scraper](https://docs.emelia.io/docs/linkedin-scraper.md) `PUT https://api.emelia.io/linkedin-scrappers/authes/{id}` Replaces the session cookies of a scraping account (its `authId`), typically after a scraper stopped with `INVALID_COOKIE` or when the account shows `status: invalid`. `status` is set back to `valid` (the cookies are not tested at this moment). Then restart the stopped scraper with `PUT /linkedin-scrappers/{scrapperId}/resume`. Send the complete set every time: `li_a`, `ua`, `viewport`, `country` and `additionnalsCookie` that you omit are cleared, and so is the LinkedIn profile shown in the app. Returns the same account id. Only scraping accounts can be updated here (not campaign accounts). ## Authentication Send your API key in the `Authorization` header: `Authorization: `. ## Path parameters | Name | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Account id (the `authId` returned by `POST /linkedin-scrappers/authes`). 24 hexadecimal characters. | ## Request body Content type: `application/json`, required. Session cookies of a LinkedIn account logged in to Sales Navigator. | Field | Type | Required | Description | |---|---|---|---| | `li_at` | string | yes | Value of the `li_at` cookie (LinkedIn session). Required. Min length 1. | | `li_a` | string | no | Value of the `li_a` cookie (Sales Navigator session). | | `jsessionid` | string | no | Accepted but ignored. Send `JSESSIONID` in `additionnalsCookie`. | | `ua` | string | no | User agent of the browser the cookies come from. | | `additionnalsCookie` | array of object or string | no | Other linkedin.com cookies of the same session (spelling with two n is the real field name). `li_at`, `li_a` and `JSESSIONID` entries are dropped (the latter is then lost). | | `viewport` | object | no | Window size of the browser the cookies come from. Both values required when the object is sent. | | `viewport.width` | number | yes | | | `viewport.height` | number | yes | | | `country` | string | no | ISO 3166-1 alpha-2 code of the country the account is used from (for example `FR`). Emelia browses LinkedIn through a proxy located in this country. | ```json { "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" } ``` ## Example request **cURL** ```bash curl -X PUT "https://api.emelia.io/linkedin-scrappers/authes/66e0f1a2b3c4d5e6f7a8b9c0" \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" }' ``` **JavaScript** ```js const response = await fetch('https://api.emelia.io/linkedin-scrappers/authes/66e0f1a2b3c4d5e6f7a8b9c0', { method: 'PUT', headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" }), }); console.log(await response.json()); ``` **Python** ```python import os import requests response = requests.put( "https://api.emelia.io/linkedin-scrappers/authes/66e0f1a2b3c4d5e6f7a8b9c0", headers={"Authorization": os.environ["EMELIA_API_KEY"]}, json={ "li_at": "AQEDAFakeLiAtCookieForDocsOnly0000000000", "li_a": "AQJ2FakeLiACookieForDocsOnly0000000000", "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "additionnalsCookie": [ { "name": "JSESSIONID", "value": "\"ajax:0000000000000000000\"", "domain": ".www.linkedin.com" }, { "name": "lang", "value": "v=2&lang=fr-fr", "domain": ".linkedin.com" }, { "name": "bcookie", "value": "\"v=2&00000000-fake-0000-0000-000000000000\"", "domain": ".linkedin.com" } ], "viewport": { "width": 1440, "height": 900 }, "country": "FR" }, ) print(response.json()) ``` ## Responses ### 200: Cookies replaced, account valid again. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | | | `authId` | string | yes | Account id. | ```json { "success": true, "authId": "66e0f1a2b3c4d5e6f7a8b9c0" } ``` ### 400: Invalid body or unknown account. | 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": "string", "code": "INVALID_REQUEST", "params": {} } ``` ### 401: Missing or invalid API key (`Authorization` header). | 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" } ``` ### 404: The id is not 24 hexadecimal characters: no route matches and the server answers with its default HTML page (not JSON). Content type: `text/html`. ### 500: Unexpected server error. Current behavior: also returned when the per-minute API quota of the workspace is exceeded (30 requests per minute without subscription, 100 on Start, 300 on Grow, 1,000 on Scale). A 429 would be expected; retry after a minute. | 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" } ``` --- # AI & integrations > Use Emelia from Claude, Cursor or VS Code, give your assistant the whole API in one file, and build faster with ready-made prompts. Source: https://docs.emelia.io/docs/ai Emelia is built to be driven by AI agents as much as by code. Pick what fits how you work: | You want to | Use | | ------------------------------------------------------------- | ----------------------------------------------------- | | Ask an assistant to manage your campaigns, lists and contacts | The [MCP server](https://docs.emelia.io/docs/ai/mcp.md) | | Run your whole outbound from Claude Code, with a method | The [Claude Code plugin](https://docs.emelia.io/docs/ai/claude-code-plugin.md) | | Have Claude write correct code against the Emelia API | The [Claude skill](https://docs.emelia.io/docs/ai/claude-skill.md) | | Get working code from any assistant in minutes | [Build with AI](https://docs.emelia.io/docs/ai/build-with-ai.md) | | Feed these docs to an agent | [Docs for AI agents](https://docs.emelia.io/docs/ai/llms.md) | - [MCP server](https://docs.emelia.io/docs/ai/mcp): 26 tools: campaigns, lists, contacts, enrichment, deliverability. One URL, your API key. - [Claude Code plugin](https://docs.emelia.io/docs/ai/claude-code-plugin): claude-outreach: from the ideal customer profile to the replies, step by step. - [Claude skill](https://docs.emelia.io/docs/ai/claude-skill): One SKILL.md that teaches Claude the API, its rules and its traps. - [Build with AI](https://docs.emelia.io/docs/ai/build-with-ai): Prompts ready to paste for the most common integrations. - [Docs for AI agents](https://docs.emelia.io/docs/ai/llms): /llms.txt, /llms-full.txt, every page in Markdown, the OpenAPI specs. > **Your assistant acts with your key.** Whatever the tool, the assistant uses your API key: it can change your campaigns and spend your credits. Keep the confirmations on for actions that send messages, delete data or use credits. --- # MCP server > Connect Claude Code, Claude Desktop, Cursor, VS Code or any MCP client to your Emelia account. 26 tools for campaigns, lists, contacts, enrichment and deliverability. Source: https://docs.emelia.io/docs/ai/mcp The Emelia MCP server lets an AI assistant work in your Emelia account in plain language: "add these three prospects to my SaaS founders list", "what is the reply rate of my Q4 campaign this week?", "which of my mailboxes are disconnected?". | | | | -------------- | ------------------------------------------------------------------------------------------------------- | | URL | `https://mcp.emelia.io/mcp` | | Transport | Streamable HTTP, stateless (`POST` only) | | Authentication | Your Emelia API key in the `Authorization` header (raw or `Bearer `), or in `X-Api-Key`. No OAuth. | | Health check | `https://mcp.emelia.io/mcp/health` answers `{"ok":true}` | You need an API key first: [Get your API key](https://v3.emelia.io/settings/api) Whatever the client, name the server `emelia`: the tools then show up as `mcp__emelia__`, the names the [Claude Code plugin](https://docs.emelia.io/docs/ai/claude-code-plugin.md) expects. ## Install **Claude Code** For yourself, in every project: ```bash claude mcp add --transport http --scope user emelia https://mcp.emelia.io/mcp \ --header "Authorization: YOUR_EMELIA_API_KEY" ``` For a shared project, commit a `.mcp.json` at the root. It references the variable, not the key: each person keeps the key in their environment (`export EMELIA_API_KEY=YOUR_EMELIA_API_KEY`). ```json title=".mcp.json" { "mcpServers": { "emelia": { "type": "http", "url": "https://mcp.emelia.io/mcp", "headers": { "Authorization": "${EMELIA_API_KEY}" } } } } ``` Run `/mcp`: `emelia` should show as connected. That does not prove the key is right (the server checks it at the first tool call): ask "list my campaigns" to test it. If `/mcp` shows "needs authentication", the header does not reach the server. **Claude Desktop** **Custom connector**, nothing to install: **Customize, Connectors, Add, Add custom connector** (Pro and Max plans; on Team and Enterprise an owner adds it in the organization settings). * URL: `https://mcp.emelia.io/mcp` * Authentication: no OAuth sign in. * Request headers: name `Authorization`, value `YOUR_EMELIA_API_KEY`. The calls then come from Anthropic's cloud, not from your computer. On Team and Enterprise, the header is set once for the whole organization: every member acts on the same Emelia account. **Or the configuration file** (`claude_desktop_config.json`, opened from **Settings, Developer, Edit Config**; quit and restart the app after saving). This file only starts local servers, so it uses the `mcp-remote` bridge, which needs Node 18 or later: ```json title="claude_desktop_config.json" { "mcpServers": { "emelia": { "command": "npx", "args": [ "-y", "mcp-remote", "https://mcp.emelia.io/mcp", "--header", "Authorization:${EMELIA_API_KEY}" ], "env": { "EMELIA_API_KEY": "YOUR_EMELIA_API_KEY" } } } } ``` **Cursor** [Install in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=emelia&config=eyJ1cmwiOiJodHRwczovL21jcC5lbWVsaWEuaW8vbWNwIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiWU9VUl9FTUVMSUFfQVBJX0tFWSJ9fQ%3D%3D) The button opens Cursor with the server ready to add. Then replace `YOUR_EMELIA_API_KEY` with your key in the MCP configuration of Cursor. Or add it by hand to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project): ```json title="~/.cursor/mcp.json" { "mcpServers": { "emelia": { "url": "https://mcp.emelia.io/mcp", "headers": { "Authorization": "YOUR_EMELIA_API_KEY" } } } } ``` To keep the key out of the file, write `"Authorization": "${env:EMELIA_API_KEY}"`. **VS Code** [Install in VS Code](https://insiders.vscode.dev/redirect/mcp/install?name=emelia&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.emelia.io%2Fmcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22YOUR_EMELIA_API_KEY%22%7D%7D) The button opens VS Code with the server ready to install. Then replace `YOUR_EMELIA_API_KEY` with your key in the configuration it creates. To keep the key out of the file, configure it in `.vscode/mcp.json` (or with the command **MCP: Open User Configuration**): VS Code asks for the key the first time the server starts and stores it securely. ```json title=".vscode/mcp.json" { "inputs": [ { "type": "promptString", "id": "emelia-api-key", "description": "Emelia API key", "password": true } ], "servers": { "emelia": { "type": "http", "url": "https://mcp.emelia.io/mcp", "headers": { "Authorization": "${input:emelia-api-key}" } } } } ``` **ChatGPT** > **Not available.** ChatGPT connectors sign in with OAuth or without authentication, and the Emelia MCP server needs your API key in a header. ChatGPT cannot connect to it today. Use Claude, Cursor or VS Code. **Other clients** Any client that supports remote MCP servers over Streamable HTTP with a custom header works: * URL: `https://mcp.emelia.io/mcp` * Header: `Authorization: `, or `X-Api-Key: ` if your client reserves `Authorization`. Every request must accept both `application/json` and `text/event-stream`. To check the server and your key by hand: ```bash curl -s https://mcp.emelia.io/mcp \ -H "Authorization: $EMELIA_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` Clients that can only sign in to remote servers with OAuth cannot connect. ## Tools The server relays each tool to the [REST API](https://docs.emelia.io/docs/all-endpoints.md) with your key. Tools marked **credits** spend Emelia credits ([prices](https://docs.emelia.io/docs/credits.md)). Tools marked **destructive** delete data for good. ### Campaigns | Tool | What it does | | ------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `list_campaigns` | All campaigns of the current app (not the legacy ones), with id, name and status. Archived campaigns are left out. | | `get_campaign` | Full configuration: status, schedule, steps, attached and excluded lists. | | `create_campaign` | Creates a draft campaign from a name, with the default settings. | | `get_campaign_stats` | Sent, opens, clicks, replies, bounces, globally and per step, optionally between two dates. | | `get_campaign_activities` | The 30 most recent activities, replies included, filtered by type, contact or version. | | `list_manual_tasks` | Pending manual tasks of a campaign. | | `complete_manual_task` | Marks a manual task `ok` or `ko`. | ### Lists and contacts | Tool | What it does | | --------------------------- | ------------------------------------------------------------------------------- | | `create_list` | Creates an empty list. | | `list_lists` | Your lists, paginated, with name search and filters. | | `get_list` | One list and its counters. | | `rename_list` | Renames a list. | | `delete_list` | **Destructive.** Deletes a list and all its contacts. | | `get_list_contacts` | Contacts of a list, or an exact lookup by email or LinkedIn URL. | | `get_contact` | One contact of a list. | | `add_contact_to_list` | Adds one contact; it enters the campaigns of the list. Duplicates are detected. | | `add_contacts_to_list_bulk` | Adds up to 100 contacts in one call. | | `update_contact` | Updates the fields of a contact. | | `delete_contact` | **Destructive.** Deletes a contact, and removes it from campaigns. | | `find_contact` | Looks a contact up across all your lists, by email or LinkedIn URL. | ### Enrichment | Tool | What it does | | ----------------------- | ---------------------------------------------------------------------------------- | | `find_email` | **Credits.** Finds a professional email from a full name, a company and a country. | | `find_phone` | **Credits.** Finds a phone number from a LinkedIn profile URL. | | `verify_email` | **Credits.** Verifies an email address. | | `get_enrichment_result` | Reads a job that was still running. | The three enrichment tools wait up to 90 seconds for the result. A longer job comes back with its `jobId` and `status: "running"`: ask the assistant to check it later with `get_enrichment_result`. ### Deliverability | Tool | What it does | | ---------------------- | ------------------------------------------------------------------------------ | | `list_email_providers` | Your sending mailboxes, passwords masked. Can list only the disconnected ones. | | `get_warmup_status` | Warmup of each mailbox: running or not, score, sent, received, spam. | | `set_warmup` | Turns the warmup of a mailbox on. | ### Known quirks * **`find_email` needs `country`.** The tool marks it optional, but the API refuses a search without it. Always give the country of the company. * **`set_warmup` with `enabled: false` fails** with "Warmup not found": the tool sends the id of the mailbox where the API expects the id of the warmup. Turn the warmup off in the Emelia app. * **`get_warmup_status` returns every mailbox at once**, whatever the `page` you pass. * **`get_campaign_stats` on an unknown campaign** shows "Invalid or disabled Emelia API key", because the statistics route answers `401`. Check the campaign id before your key. Its `end` date is an exact bound: a date without time excludes that day. ## Limits The server has no quota of its own: it uses the one of your account. The tools that read or change campaigns, mailboxes and warmup count in your [per minute quota](https://docs.emelia.io/docs/rate-limits.md) (30 to 1,000 requests per minute depending on your plan). The enrichment tools and the list and contact tools are not counted. Over the quota, a tool fails with "Emelia API error (500)": wait a minute. Requests to the server are limited to 4 MB, and each tool returns the whole API response, without truncation: ask for small pages on big lists. ## Troubleshooting | Symptom | Cause | | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Missing Emelia API key` (JSON-RPC error `-32001`, HTTP 401) | The header did not reach the server. Check the client configuration. | | "Invalid or disabled Emelia API key" | The key is wrong, disabled or deleted, or the campaign of `get_campaign_stats` does not exist. Create a new key in **Settings, API & Webhooks** if needed. | | "Emelia API error (500)" | Usually the per minute quota. Wait a minute. | | "Emelia API error (400): ..." with a JSON body | The API refused the request; the message says why, for example `"country" is required`. | | `405` on `/mcp` | The server only answers `POST`. A `GET` in the browser is expected to fail. | | `406` or `415` | The client does not send `Accept: application/json, text/event-stream` or `Content-Type: application/json`. | | A tool is missing | The server covers the 26 tools above. Everything else is in the [REST API](https://docs.emelia.io/docs/all-endpoints.md). | ## Prompts to try * "List my campaigns and tell me which ones are running." * "Add Claire Fontaine, CTO at Nexora, [claire.fontaine@nexora.io](mailto:claire.fontaine@nexora.io), to my **Q4 SaaS founders** list." She enters the campaigns of that list. * "What is the reply rate of my **Q4 SaaS founders** campaign since Monday, step by step?" * "Find and verify the email of Hugo Martin at Brightlane, in France, then add him to my **Warm leads** list." * "Which of my mailboxes are disconnected, and which have a low warmup score?" > **Your assistant acts with your key.** The assistant can change your campaigns and spend your credits. Keep the confirmations of your client on for tools that delete data or use credits. --- # Claude Code plugin > claude-outreach runs your whole outbound from Claude Code, from the ideal customer profile to the replies, with Emelia as the engine. Install, keys, commands and updates. Source: https://docs.emelia.io/docs/ai/claude-code-plugin [claude-outreach](https://github.com/emelia-io/claude-outreach) is an open source plugin for Claude Code, MIT licensed. It packages a method: define who you target, build and clean the list, find and verify emails and phone numbers, write a sequence that does not read like a template, check deliverability, launch in Emelia, triage the replies, and read what worked. [View on GitHub](https://github.com/emelia-io/claude-outreach) ## Install ### Add the plugin In Claude Code: ```text /plugin marketplace add emelia-io/claude-outreach /plugin install claude-outreach@claude-outreach ``` From a terminal, the same with `claude plugin marketplace add emelia-io/claude-outreach` then `claude plugin install claude-outreach@claude-outreach`. ### Add your keys The skills call the Emelia REST API with the key in your environment. In `~/.zshrc` or `~/.bashrc`: ```bash export EMELIA_API_KEY="..." # enrichment, contacts, campaigns, replies export BASILE_API_KEY="..." # optional, French B2B data ``` Open a new terminal, then start Claude Code from it: a variable exported in another terminal is not visible to it. Without a key, everything still runs in dry run: you get the list, the copy and the sequence as files, nothing is sent and nothing is charged. ### Connect the MCP server (optional) > **The plugin does not configure the MCP server.** Installing the plugin adds skills and agents, not the MCP server. Some agents of the plugin use the Emelia MCP tools when the server is there: add it yourself, under the name `emelia`. ```bash claude mcp add --transport http --scope user emelia https://mcp.emelia.io/mcp \ --header "Authorization: YOUR_EMELIA_API_KEY" ``` Keep the name `emelia`: the agents look for tools named `mcp__emelia__*`. See [MCP server](https://docs.emelia.io/docs/ai/mcp.md). **Manual install, without the plugin system** ```bash git clone https://github.com/emelia-io/claude-outreach.git cp -r claude-outreach/skills/* ~/.claude/skills/ cp -r claude-outreach/agents/* ~/.claude/agents/ ``` On Windows PowerShell: ```powershell git clone https://github.com/emelia-io/claude-outreach.git Copy-Item -Recurse claude-outreach\skills\* $HOME\.claude\skills\ Copy-Item -Recurse claude-outreach\agents\* $HOME\.claude\agents\ ``` With a manual install, the skill is called `/outreach` instead of `/claude-outreach:outreach`. ## Start Skills of a plugin are called with the name of the plugin first: ```text /claude-outreach:outreach pilot ``` A guided first campaign on 100 contacts, with a checkpoint at every step. It asks who you sell to, builds a small list, enriches it, writes the sequence with you, checks your deliverability, and launches only when you say so. Every step writes a file you keep, in an `outreach/` folder. Once you trust it: ```text /claude-outreach:outreach full "sell our API monitoring tool to CTOs of French SaaS companies, 20 to 200 people" ``` ## Commands Each command is `/claude-outreach:outreach ` (or `/outreach ` with a manual install). | Command | What it does | | -------------------- | ------------------------------------------------------------ | | `pilot` | Guided first campaign on 100 contacts | | `full ` | The complete pipeline, from targeting to analysis | | `offer ` | Turn what you sell into an offer a competitor could not copy | | `icp ` | Turn a vague brief into a targeting spec | | `leads ` | Build the list from Basile, LinkedIn or a CSV | | `filter ` | Deduplicate, exclude customers and the blacklist, segment | | `find-email ` | Find professional email addresses | | `find-phone ` | Find phone numbers | | `verify ` | Verify the addresses you brought yourself | | `enrich ` | Find what is missing, verify only what needs it | | `write ` | Subjects, openers, follow ups, break up | | `personalize ` | Icebreakers and AI variables per contact | | `sequence ` | Steps, delays, conditions, A/B tests | | `deliverability` | SPF, DKIM, DMARC, warmup, volume, ramp plan | | `campaign ` | Create and launch the campaign in Emelia | | `replies` | Triage replies, draft answers, handle opt outs | | `audit ` | Rates per step and variant, against benchmarks | ## What it will not do * Spend credits, launch a campaign or reach a human without your confirmation. * Launch when your deliverability is not ready: missing SPF, DKIM or DMARC, warmup off, or a volume above what your mailboxes can carry. * Scrape LinkedIn outside the official Emelia integration. ## Stay up to date Plugins from a third party marketplace do not update on their own. In Claude Code: ```text /plugin marketplace update claude-outreach ``` or from a terminal, `claude plugin update claude-outreach@claude-outreach`. With a manual install from a git clone, run `git pull` in the clone, or `bash scripts/check-update.sh` to see how far behind you are. --- # Claude skill > Download the emelia-api skill, one SKILL.md that teaches Claude the Emelia API, its rules and its traps, so the code it writes works the first time. Source: https://docs.emelia.io/docs/ai/claude-skill A [skill](https://support.claude.com/en/articles/12512180-using-skills-in-claude) is a folder of instructions that Claude loads when a task needs it. The `emelia-api` skill gives Claude what an experienced Emelia developer knows: the base URLs, the endpoints that matter, how contacts are mapped, how a campaign sequence is built, how to poll enrichment jobs, and the mistakes that fail silently. [Download emelia-api.zip](https://docs.emelia.io/skills/emelia-api.zip) [Read SKILL.md](https://docs.emelia.io/skills/emelia-api/SKILL.md) ## Install **Claude (web and desktop)** 1. Download `emelia-api.zip`. Do not unzip it. 2. In Claude, open **Customize, Skills**. 3. Click **+**, then **Create skill**, then **Upload a skill**, and select the zip. 4. The skill appears in your list, switched on. Skills need code execution to be enabled in your Claude settings. Skills you add to your Claude account are also available in Claude Code when you sign in with that account. **Claude Code** Unzip it in your skills folder, for you only or for one project: ```bash # for all your projects mkdir -p ~/.claude/skills && unzip emelia-api.zip -d ~/.claude/skills/ # or for this project only, shared with your team through git mkdir -p .claude/skills && unzip emelia-api.zip -d .claude/skills/ ``` You get `skills/emelia-api/SKILL.md`. Claude Code picks it up at the next start. ## What it covers * Authentication, base URLs, the three rules of the Emelia model. * The 30 calls you use most, in one table. * How contact keys are mapped, and how duplicates and updates work. * The campaign step tree: IDs, versions, conditions, variables, HTML bodies. * The campaign rules that cause most failures: pause before editing, required settings, replaced arrays. * Enrichment jobs, polling and credits. * Webhooks, errors, rate limits, pagination. * Safety: confirm before sending, deleting or spending credits. ## Use it Ask Claude for what you want to build, in your words: ```text Write a Python script that reads leads.csv and loads it into my Emelia list "Q4 SaaS founders", 100 contacts per call, updating the contacts that already exist, and prints the rows that failed. ``` Claude reads the skill when the task involves the Emelia API. For anything the skill does not detail, it is told to read the Markdown pages of this documentation. > **Skill or MCP server?.** The skill teaches Claude to **write code** against the API. The [MCP server](https://docs.emelia.io/docs/ai/mcp.md) lets Claude **act** in your account directly. They work well together. --- # Build with AI > Prompts ready to paste into Claude, ChatGPT or Cursor to build the most common Emelia integrations, with the documentation they need. Source: https://docs.emelia.io/docs/ai/build-with-ai Each prompt below points the assistant to the Markdown version of the right pages, so it works from the documentation instead of from memory. Copy, paste, adjust the parts in capitals. > **Give your assistant the docs.** For long sessions, give it the whole documentation once: `https://docs.emelia.io/llms-full.txt`. In Claude, the [emelia-api skill](https://docs.emelia.io/docs/ai/claude-skill.md) does the same job automatically. ## Load a CSV into a list **Prompt: CSV to Emelia list, with updates and a failure report** ```text Read https://docs.emelia.io/docs/lists.md and https://docs.emelia.io/docs/rate-limits.md. Write a Node.js script (no dependency, Node 20+) that: 1. reads the CSV file given as first argument (columns: first_name, last_name, email, company, linkedin_url, CUSTOM_COLUMNS), 2. renames the columns to the Emelia standard fields (firstName, lastName, email, companyName, linkedinUrlProfile) and keeps the others as custom variables, 3. sends the rows to the Emelia list LIST_ID with POST /lists/list/{listId}/contacts/batch?updateIfExists=true, 100 rows per call, 4. prints the totals (created, duplicates, updated, failed) and writes the failed rows with their error to failed.csv. Read the API key from EMELIA_API_KEY and trim it. Stop and print the error on a non 2xx answer. ``` ## Sync replies to your CRM **Prompt: Webhook receiver that pushes replies to a CRM** ```text Read https://docs.emelia.io/docs/webhooks.md and https://docs.emelia.io/docs/replies.md. Build a small Express server that receives Emelia webhooks on POST /emelia/:token: - reject requests whose token is not EMELIA_WEBHOOK_TOKEN, - answer 200 immediately, then process the event, - on MAIL_REPLIED and LINKEDIN_REPLIED, find the contact by email in CRM_NAME and add a note with the reply and the campaign name, - if sentiment.classification is INTERESTED, also create a task for the owner, - deliveries are not signed and can come twice or out of order: deduplicate on event, campaign, contact email and step. ``` ## Create a campaign from a brief **Prompt: Full campaign by API, paused until I approve** ```text Read https://docs.emelia.io/docs/campaigns.md. Using the Emelia API, create a campaign named "CAMPAIGN_NAME": - a sequence of 3 emails: day 0, day 3, day 8, follow-ups in the same thread, unsubscribe link from the 2nd email, - leave out every step _id except "START", so the server generates them, - schedule Monday to Friday, 9:00 to 17:30, Europe/Paris, 20 new contacts per day, - identities: only the mailboxes of the domain SENDER_DOMAIN (list them first with GET /email-providers and ask me to confirm), - recipients: the list LIST_NAME. Read the campaign back after each call. Send me a test email at MY_EMAIL, then stop: do not start the campaign. ``` ## Enrich a list without wasting credits **Prompt: Email finder on a CSV, with cost control** ```text Read https://docs.emelia.io/docs/enrichment/email-finder.md and https://docs.emelia.io/docs/credits.md. Write a Python script that finds the professional email of every row of leads.csv (full_name, company, website, country): - skip rows without a company or a country, and say how many, - print the maximum cost in credits and ask for confirmation before starting, - start all the jobs first, then poll them every 3 to 5 seconds, with a 5 minute limit per job, - keep each jobId in the output, so a job still running can be read later, - never send the same job twice, - stop at the first "not enough credits" error, - write leads_enriched.csv with email, qualification and status. ``` ## Audit a campaign with the MCP server **Prompt: What is working, what to change** ```text Use the Emelia MCP server (https://docs.emelia.io/docs/ai/mcp.md). For my campaign "CAMPAIGN_NAME": 1. get its configuration and its statistics for the last 30 days, per step and per version, 2. tell me which step brings the replies and which one costs unsubscribes, 3. compare the A/B versions on replies, not on opens, 4. check the warmup score of the mailboxes it uses, 5. give me one recommendation, not a list. Do not change anything in the account. ``` ## Generate a typed client **Prompt: TypeScript client from the OpenAPI spec** ```text Download https://docs.emelia.io/openapi/emelia.json. Generate a small, dependency free TypeScript client for these endpoints only: create a list, add contacts in bulk, find a contact by email, update a contact, create a campaign, update its recipients, start and pause it. - read the key from an option, send it raw in the Authorization header, - throw an error that carries the HTTP status and the `code` and `error` (or `message`, on /lists routes) of the body, - treat an HTTP 500 {"error": "Internal server error"} as the per minute quota: wait 60 seconds and retry once, - add a `paginate` helper for GET /lists/list/{listId}/rows (page from 1, pageSize 100). ``` ## Tips * **Point to pages, not to the whole site.** Two or three `.md` pages give better code than a vague "use the Emelia API". * **Ask it to read back.** After each write, the campaign or the contact read back shows what Emelia actually stored. * **Keep the confirmations.** Ask the assistant to stop before anything that sends a message, deletes data or spends credits. --- # Docs for AI agents > llms.txt, llms-full.txt, a Markdown version of every page and the OpenAPI specs. Everything an agent needs to read these docs. Source: https://docs.emelia.io/docs/ai/llms These docs are written for people and for agents. Everything below is public, needs no key, and is regenerated at every deploy. | Resource | URL | Use it for | | --------------- | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | Index | [/llms.txt](https://docs.emelia.io/llms.txt) | The map of the docs, every page with a one line summary and a link to its Markdown. | | Full text | [/llms-full.txt](https://docs.emelia.io/llms-full.txt) | Every page in one Markdown file, in the order of the sidebar. | | One page | Add `.md` to any URL, for example [/docs/quickstart.md](https://docs.emelia.io/docs/quickstart.md.md) | The exact page, without the site around it. | | Emelia API spec | [/openapi/emelia.json](https://docs.emelia.io/openapi/emelia.json) | OpenAPI 3: code generation, Postman, Insomnia, agents. | | Claude skill | [/skills/emelia-api.zip](https://docs.emelia.io/skills/emelia-api.zip) | The API rules in one file, see [Claude skill](https://docs.emelia.io/docs/ai/claude-skill.md). | ## On every page The **Copy page** menu, at the top of each page, gives the same resources in one click: * **Copy page**: copies the page as Markdown, ready to paste in a chat. * **View as Markdown**: opens the `.md` version. * **Open in Claude**, **Open in ChatGPT**, **Open in Cursor**: opens the assistant with a prompt that points to the Markdown version of the page. Each page also declares its Markdown version in its HTML head (``), so a crawler finds it without guessing. ## In your editor Most AI editors let you add documentation by URL. Give them `https://docs.emelia.io/llms-full.txt`, or a single `.md` page for a focused task. ## Route pages The Markdown version of a route page is generated from the OpenAPI spec: method and URL, a link to its guide, parameters, request body fields, examples in cURL, JavaScript and Python, and the responses with their fields. For example: [/docs/lists/add-contacts-in-bulk.md](https://docs.emelia.io/docs/lists/add-contacts-in-bulk.md.md). In `/llms.txt`, every route is listed under its topic with its method and path, for example `POST /lists/list/{listId}/contacts/batch`. --- # All endpoints > Every route of the Emelia API on one page, grouped by topic, with a link to its reference page and playground, and the OpenAPI specs to download. Source: https://docs.emelia.io/docs/all-endpoints Every route below has its own page, under its topic in the menu: parameters, request body, responses with realistic examples, code in cURL, JavaScript and Python, and a playground to send real requests with your key. - Emelia API: base URL `https://api.emelia.io`, 114 routes. OpenAPI: https://docs.emelia.io/openapi/emelia.json The specs are OpenAPI 3.1 files: import them in Postman or Insomnia, generate a client, or give them to an AI agent. ## Emelia API **Credits** - [GET /credits/usage](https://docs.emelia.io/docs/credits/list-credit-debits.md): List credit debits **Campaigns** - [GET /advanced/campaigns](https://docs.emelia.io/docs/campaigns/list-campaigns.md): List campaigns - [POST /advanced/campaigns](https://docs.emelia.io/docs/campaigns/create-campaign.md): Create a campaign - [GET /advanced/campaigns/{campaignId}](https://docs.emelia.io/docs/campaigns/get-campaign.md): Get a campaign - [PATCH /advanced/campaigns/{campaignId}/steps](https://docs.emelia.io/docs/campaigns/replace-sequence.md): Replace the sequence - [PATCH /advanced/campaigns/{campaignId}/settings](https://docs.emelia.io/docs/campaigns/update-campaign-settings.md): Update campaign settings - [PATCH /advanced/campaigns/{campaignId}/identities](https://docs.emelia.io/docs/campaigns/set-sender-identities.md): Set the sender identities - [PATCH /advanced/campaigns/{campaignId}/recipients](https://docs.emelia.io/docs/campaigns/set-contact-lists.md): Set the contact lists of a campaign - [POST /advanced/campaigns/{campaignId}/test-email](https://docs.emelia.io/docs/campaigns/send-test-email.md): Send a test email - [POST /advanced/campaigns/{campaignId}/start](https://docs.emelia.io/docs/campaigns/start-campaign.md): Start a campaign - [PATCH /advanced/campaigns/{campaignId}/planned-start](https://docs.emelia.io/docs/campaigns/schedule-planned-start.md): Schedule or cancel a planned start - [POST /advanced/campaigns/{campaignId}/pause](https://docs.emelia.io/docs/campaigns/pause-campaign.md): Pause a campaign - [POST /advanced/campaigns/{campaignId}/finish](https://docs.emelia.io/docs/campaigns/mark-campaign-as-finished.md): Mark a campaign as finished - [POST /advanced/campaigns/{campaignId}/reactivate](https://docs.emelia.io/docs/campaigns/reactivate-campaign.md): Reactivate a finished campaign - [PATCH /advanced/campaigns/{campaignId}/name](https://docs.emelia.io/docs/campaigns/rename-campaign.md): Rename a campaign - [POST /advanced/campaigns/{campaignId}/duplicate](https://docs.emelia.io/docs/campaigns/duplicate-campaign.md): Duplicate a campaign - [POST /advanced/campaigns/{campaignId}/archive](https://docs.emelia.io/docs/campaigns/archive-campaign.md): Archive a campaign - [DELETE /advanced/campaigns/{campaignId}](https://docs.emelia.io/docs/campaigns/delete-campaign.md): Delete a campaign - [GET /advanced/campaign/contacts](https://docs.emelia.io/docs/campaigns/list-contacts-of-campaign.md): List the contacts of a campaign - [POST /advanced/campaign/contacts](https://docs.emelia.io/docs/campaigns/add-contact-to-campaign.md): Add a contact to a campaign - [DELETE /advanced/campaign/contacts](https://docs.emelia.io/docs/campaigns/remove-contact-from-campaign.md): Remove a contact from a campaign - [PATCH /advanced/contacts](https://docs.emelia.io/docs/campaigns/set-contact-field.md): Set a field on a campaign contact - [GET /advanced/campaigns/{campaignId}/manual-tasks](https://docs.emelia.io/docs/campaigns/list-pending-manual-tasks.md): List pending manual tasks - [POST /advanced/campaigns/{campaignId}/manual-tasks/status](https://docs.emelia.io/docs/campaigns/complete-manual-task.md): Complete a manual task - [GET /advanced/campaigns/{campaignId}/progress](https://docs.emelia.io/docs/campaigns/get-per-step-progress.md): Get per step progress - [GET /advanced/campaigns/{campaignId}/progress/contacts](https://docs.emelia.io/docs/campaigns/list-contacts-of-step.md): List the contacts of a step - [GET /advanced/campaigns/{campaignId}/export](https://docs.emelia.io/docs/campaigns/export-as-csv.md): Export campaign data as CSV - [GET /stats/advanced](https://docs.emelia.io/docs/campaigns/get-campaign-statistics.md): Get campaign statistics - [POST /advanced/attachments](https://docs.emelia.io/docs/campaigns/upload-message-attachments.md): Upload message attachments - [POST /advanced/linkedin/audio](https://docs.emelia.io/docs/campaigns/upload-linkedin-voice-note.md): Upload a LinkedIn voice note **Campaigns / Legacy / Email campaigns** - [GET /emails/campaign/contacts](https://docs.emelia.io/docs/campaigns/legacy/email/list-campaign-contacts.md): List email campaign contacts (paginated) (deprecated) - [POST /emails/campaign/contacts](https://docs.emelia.io/docs/campaigns/legacy/email/add-contact.md): Add a contact to an email campaign (deprecated) - [DELETE /emails/campaign/contacts](https://docs.emelia.io/docs/campaigns/legacy/email/remove-contact.md): Remove a contact from an email campaign (deprecated) - [POST /emails/lists/contacts](https://docs.emelia.io/docs/campaigns/legacy/email/add-contact-to-list.md): Add a contact to an email list (deprecated) - [GET /emails/campaigns](https://docs.emelia.io/docs/campaigns/legacy/email/list-campaigns.md): List email campaigns (deprecated) - [POST /emails/campaigns](https://docs.emelia.io/docs/campaigns/legacy/email/create-campaign.md): Create an email campaign (deprecated) - [GET /emails/campaigns/{campaignId}](https://docs.emelia.io/docs/campaigns/legacy/email/get-campaign.md): Get email campaign details (deprecated) - [PATCH /emails/campaigns/{campaignId}/settings/global](https://docs.emelia.io/docs/campaigns/legacy/email/update-schedule.md): Update email campaign schedule (global settings) (deprecated) - [PATCH /emails/campaigns/{campaignId}/settings/providers](https://docs.emelia.io/docs/campaigns/legacy/email/update-mailboxes.md): Update email campaign sending providers (deprecated) - [PATCH /emails/campaigns/{campaignId}/name](https://docs.emelia.io/docs/campaigns/legacy/email/rename-campaign.md): Rename an email campaign (deprecated) - [PATCH /emails/campaigns/{campaignId}/steps](https://docs.emelia.io/docs/campaigns/legacy/email/replace-steps.md): Replace email campaign steps (deprecated) - [POST /emails/campaigns/{campaignId}/start](https://docs.emelia.io/docs/campaigns/legacy/email/start-campaign.md): Start an email campaign (deprecated) - [POST /emails/campaigns/{campaignId}/pause](https://docs.emelia.io/docs/campaigns/legacy/email/pause-campaign.md): Pause an email campaign (deprecated) - [PATCH /emails/contacts](https://docs.emelia.io/docs/campaigns/legacy/email/set-contact-field.md): Set a custom field on an email contact (deprecated) - [GET /emails/campaigns/{campaignId}/activities](https://docs.emelia.io/docs/campaigns/legacy/email/get-activities.md): Get email campaign activities (deprecated) - [POST /emails/test](https://docs.emelia.io/docs/campaigns/legacy/email/send-test-email.md): Send a test email (deprecated) - [GET /stats](https://docs.emelia.io/docs/campaigns/legacy/email/get-statistics.md): Get email campaign statistics (deprecated) **Campaigns / Legacy / LinkedIn campaigns** - [GET /linkedin/campaign/contacts](https://docs.emelia.io/docs/campaigns/legacy/linkedin/list-campaign-contacts.md): List LinkedIn campaign contacts (paginated) (deprecated) - [POST /linkedin/campaign/contacts](https://docs.emelia.io/docs/campaigns/legacy/linkedin/add-contact.md): Add a contact to a LinkedIn campaign (deprecated) - [DELETE /linkedin/campaign/contacts](https://docs.emelia.io/docs/campaigns/legacy/linkedin/remove-contact.md): Remove a contact from a LinkedIn campaign (deprecated) - [GET /linkedin/campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin/list-campaigns.md): List LinkedIn campaigns (deprecated) - [POST /linkedin/campaigns](https://docs.emelia.io/docs/campaigns/legacy/linkedin/create-campaign.md): Create a LinkedIn campaign (deprecated) - [PATCH /linkedin/campaigns/{campaignId}/settings/global](https://docs.emelia.io/docs/campaigns/legacy/linkedin/update-schedule.md): Update LinkedIn campaign schedule (global settings) (deprecated) - [PATCH /linkedin/campaigns/{campaignId}/name](https://docs.emelia.io/docs/campaigns/legacy/linkedin/rename-campaign.md): Rename a LinkedIn campaign (deprecated) - [PATCH /linkedin/campaigns/{campaignId}/steps](https://docs.emelia.io/docs/campaigns/legacy/linkedin/replace-steps.md): Replace LinkedIn campaign steps (deprecated) - [GET /linkedin/campaigns/{campaignId}/activities](https://docs.emelia.io/docs/campaigns/legacy/linkedin/get-activities.md): Get LinkedIn campaign activities (deprecated) - [GET /stats/linkedin](https://docs.emelia.io/docs/campaigns/legacy/linkedin/get-statistics.md): Get LinkedIn campaign statistics (deprecated) **Lists & contacts** - [GET /lists/list](https://docs.emelia.io/docs/lists/list-your-lists.md): List your lists - [POST /lists/list](https://docs.emelia.io/docs/lists/create-list.md): Create a list - [GET /lists/list/{listId}](https://docs.emelia.io/docs/lists/get-list.md): Get a list - [PATCH /lists/list/{listId}](https://docs.emelia.io/docs/lists/rename-list.md): Rename a list - [DELETE /lists/list/{listId}](https://docs.emelia.io/docs/lists/delete-list.md): Delete a list - [POST /lists/list/export](https://docs.emelia.io/docs/lists/export-lists-as-csv.md): Export lists as CSV - [GET /lists/list/campaign-usage](https://docs.emelia.io/docs/lists/find-campaigns-of-lists.md): Find the campaigns using lists - [GET /lists/list/{listId}/rows](https://docs.emelia.io/docs/lists/list-or-find-contacts.md): List or find the contacts of a list - [DELETE /lists/list/{listId}/rows/{leadId}](https://docs.emelia.io/docs/lists/delete-contact.md): Delete a contact - [POST /lists/list/{listId}/contacts](https://docs.emelia.io/docs/lists/add-contact.md): Add a contact to a list (add or update) - [POST /lists/list/{listId}/contacts/batch](https://docs.emelia.io/docs/lists/add-contacts-in-bulk.md): Add up to 100 contacts to a list - [PATCH /lists/contacts/{contactId}](https://docs.emelia.io/docs/lists/update-contact.md): Update a contact - [POST /lists/custom-variable](https://docs.emelia.io/docs/lists/create-custom-variable.md): Create a custom variable - [GET /lists/custom-variable/check](https://docs.emelia.io/docs/lists/check-custom-variable.md): Check whether a custom variable exists - [GET /lists/list/export/jobs/{jobId}](https://docs.emelia.io/docs/lists/get-status-of-export.md): Get the status of an export - [GET /lists/list/export/jobs/{jobId}/download](https://docs.emelia.io/docs/lists/download-finished-export.md): Download a finished export **Enrichment / Email Finder** - [POST /tools/find/email](https://docs.emelia.io/docs/enrichment/email-finder/find-email.md): Find the email of a person - [GET /tools/find/email/{jobId}](https://docs.emelia.io/docs/enrichment/email-finder/get-result.md): Get an email search result - [GET /tools/find/email](https://docs.emelia.io/docs/enrichment/email-finder/list-past-searches.md): List email searches **Enrichment / Phone Finder** - [POST /tools/find/phone](https://docs.emelia.io/docs/enrichment/phone-finder/find-phone-number.md): Find the phone number of a person - [GET /tools/find/phone/{jobId}](https://docs.emelia.io/docs/enrichment/phone-finder/get-result.md): Get a phone search result - [GET /tools/find/phone](https://docs.emelia.io/docs/enrichment/phone-finder/list-past-searches.md): List phone searches **Enrichment / Email Verifier** - [POST /tools/verify/email](https://docs.emelia.io/docs/enrichment/email-verifier/verify-email.md): Verify an email address - [GET /tools/verify/email/{jobId}](https://docs.emelia.io/docs/enrichment/email-verifier/get-result.md): Get an email verification result - [GET /tools/verify/email](https://docs.emelia.io/docs/enrichment/email-verifier/list-past-verifications.md): List email verifications **Replies** - [GET /advanced/campaigns/{campaignId}/activities](https://docs.emelia.io/docs/replies/list-campaign-activities.md): List campaign activities - [GET /advanced/campaigns/{campaignId}/activities/{activityId}/message](https://docs.emelia.io/docs/replies/get-sent-message.md): Get the message of a send activity - [POST /emails/reply](https://docs.emelia.io/docs/replies/send-reply.md): Send a reply from a connected mailbox (deprecated) **Blacklist** - [POST /emails/blacklists/contact](https://docs.emelia.io/docs/blacklist/blacklist-email-or-domain.md): Add an email or domain to the account blacklist - [DELETE /emails/blacklists/contact](https://docs.emelia.io/docs/blacklist/remove-email-or-domain.md): Remove an email or domain from the account blacklist - [GET /emails/blacklists](https://docs.emelia.io/docs/blacklist/get-blacklists.md): Get the account blacklists - [POST /emails/blacklists/linkedin](https://docs.emelia.io/docs/blacklist/blacklist-linkedin-profile.md): Add a LinkedIn profile to the blacklist - [DELETE /emails/blacklists/linkedin](https://docs.emelia.io/docs/blacklist/remove-linkedin-profile.md): Remove a LinkedIn profile from the blacklist **Webhooks** - [GET /webhook](https://docs.emelia.io/docs/webhooks/list-webhooks.md): List webhooks - [Webhook event advancedCampaignActivity](https://docs.emelia.io/docs/webhooks/campaign-activity.md): Activity on an advanced (multichannel) campaign - [Webhook event campaignEnding](https://docs.emelia.io/docs/webhooks/campaign-finished.md): Campaign finished - [Webhook event emailCampaignActivity](https://docs.emelia.io/docs/webhooks/legacy-email-activity.md): Activity on a legacy email campaign - [Webhook event linkedinCampaignActivity](https://docs.emelia.io/docs/webhooks/legacy-linkedin-activity.md): Activity on a legacy LinkedIn campaign **Email providers** - [GET /email-providers](https://docs.emelia.io/docs/email-providers/list-email-providers.md): List email providers - [POST /email-providers](https://docs.emelia.io/docs/email-providers/connect-mailbox.md): Connect an SMTP/IMAP mailbox - [PATCH /email-providers/{providerId}](https://docs.emelia.io/docs/email-providers/update-mailbox.md): Update an SMTP/IMAP mailbox - [PATCH /email-providers/{providerId}/sender-name](https://docs.emelia.io/docs/email-providers/change-sender-name.md): Change the sender name - [DELETE /email-providers/{providerId}](https://docs.emelia.io/docs/email-providers/delete-mailbox.md): Delete a mailbox - [GET /email-providers/warmup](https://docs.emelia.io/docs/email-providers/list-warmups.md): List warmups - [POST /email-providers/warmup/enable](https://docs.emelia.io/docs/email-providers/enable-warmup.md): Enable the warmup of a mailbox - [POST /email-providers/warmup/check-health](https://docs.emelia.io/docs/email-providers/run-deliverability-check.md): Run a deliverability diagnosis **LinkedIn scraper** - [POST /linkedin-scrappers](https://docs.emelia.io/docs/linkedin-scraper/start-extraction.md): Create a LinkedIn scraper (start an extraction) - [GET /linkedin-scrappers](https://docs.emelia.io/docs/linkedin-scraper/list-scrapers.md): List your LinkedIn scrapers - [GET /linkedin-scrappers/{id}](https://docs.emelia.io/docs/linkedin-scraper/get-scraper.md): Get one LinkedIn scraper (poll its progress) - [GET /linkedin-scrappers/{id}/download](https://docs.emelia.io/docs/linkedin-scraper/download-csv.md): Download the contacts of a scraper (CSV) - [POST /linkedin-scrappers/{id}/enrich](https://docs.emelia.io/docs/linkedin-scraper/find-emails.md): Find the emails of a finished scraper - [POST /linkedin-scrappers/{id}/filter](https://docs.emelia.io/docs/linkedin-scraper/remove-known-contacts.md): Remove contacts already extracted by your other scrapers - [PUT /linkedin-scrappers/{id}/pause](https://docs.emelia.io/docs/linkedin-scraper/pause-extraction.md): Pause a segmented extraction - [PUT /linkedin-scrappers/{id}/resume](https://docs.emelia.io/docs/linkedin-scraper/resume-extraction.md): Resume a paused, scheduled or failed scraper - [POST /linkedin-scrappers/{id}/split](https://docs.emelia.io/docs/linkedin-scraper/split-extraction.md): Detach the part already extracted from a segmented extraction - [PUT /linkedin-scrappers/{id}/rename](https://docs.emelia.io/docs/linkedin-scraper/rename-scraper.md): Rename a LinkedIn scraper - [DELETE /linkedin-scrappers/{id}](https://docs.emelia.io/docs/linkedin-scraper/delete-scraper.md): Delete a LinkedIn scraper - [POST /linkedin-scrappers/{id}/webhook](https://docs.emelia.io/docs/linkedin-scraper/add-webhook.md): Register a webhook on a scraper - [DELETE /linkedin-scrappers/{id}/webhook](https://docs.emelia.io/docs/linkedin-scraper/remove-webhook.md): Remove a webhook from a scraper - [POST /linkedin-scrappers/authes](https://docs.emelia.io/docs/linkedin-scraper/connect-account.md): Connect a LinkedIn account for scraping (session cookies) - [PUT /linkedin-scrappers/authes/{id}](https://docs.emelia.io/docs/linkedin-scraper/refresh-account.md): Refresh the cookies of a connected scraping account --- # Changelog > Changes to the Emelia API, the MCP server and this documentation. Source: https://docs.emelia.io/docs/changelog ## October 2026 ### A new documentation, rebuilt from the code The developer documentation was rebuilt from scratch, from a read of the code of the API. * **One menu**: every topic opens its guide and lists its routes right below, with their method. * **114 routes documented**, against 92 before. Newly documented: replacing the sequence of a campaign, the test email, the progress per step, the CSV export of a campaign, the message of a send activity, finishing and reactivating a campaign, uploads of attachments and voice notes, the custom variables and the asynchronous exports of lists, the history of enrichment searches and of credit debits, the LinkedIn blacklist, the progress of one LinkedIn extraction, the filter of extractions and the refresh of scraping accounts, and more. * **Realistic examples** on every route, with code in cURL, JavaScript and Python. * **Real behaviour, documented as is**: the per minute quota answers `500` today, "not found" is a `400` on the main API, the pagination starts at 0 or 1 depending on the route. * **Corrections of the previous documentation**: `country` is required by the Email Finder; the webhook event is `EMAIL_SENT`, not `MAIL_SENT`; the contacts of a legacy campaign come back as `{ list, count }`. * **Legacy campaigns**: the email only and LinkedIn only routes are grouped under [Campaigns, Legacy](https://docs.emelia.io/docs/campaigns/legacy.md) and marked deprecated. * **AI & integrations**: the [MCP server](https://docs.emelia.io/docs/ai/mcp.md) with its 26 tools and ready to paste configurations for Claude Code, Claude Desktop, Cursor and VS Code, the [Claude Code plugin](https://docs.emelia.io/docs/ai/claude-code-plugin.md), a downloadable [Claude skill](https://docs.emelia.io/docs/ai/claude-skill.md), and [prompts](https://docs.emelia.io/docs/ai/build-with-ai.md) ready to paste. * **For agents**: [/llms.txt](https://docs.emelia.io/llms.txt), [/llms-full.txt](https://docs.emelia.io/llms-full.txt), a Markdown version of every page, and the OpenAPI spec at [/openapi/emelia.json](https://docs.emelia.io/openapi/emelia.json). * The links of the previous documentation (`/docs/emelia/...`) redirect to their new page. ## August 2026 * **MCP server** at `https://mcp.emelia.io/mcp`, with 26 tools for campaigns, lists, contacts, enrichment and deliverability. ## Earlier * **Lists routes for automations**: create a list, add up to 100 contacts in one call, add or update with `updateIfExists`, look a contact up by email or LinkedIn URL, update and delete a contact.