# 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: <your API key>`.

## 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"
}
```
