# Create a connection link

Source: https://docs.emelia.io/docs/linkedin-accounts/create-connection-link

Guide: [Create a link](https://docs.emelia.io/docs/linkedin-accounts.md#create-a-link)

`POST https://api.emelia.io/linkedin/invitations`

Creates a secure link to connect a LinkedIn account. Send `sendEmail: false` to get the link and forward it yourself: the person opens it, logs in to LinkedIn on the Emelia page and answers the verification if LinkedIn asks for one. You never see the credentials. The account then appears in `GET /linkedin/accounts`.

The link created with `sendEmail: false` is valid for 7 days. Asking again for the same `email` returns the same link with a new expiry date.

Add `authId` to reconnect an existing account: the link updates that account instead of adding a new one, so its campaigns keep it.

Without `sendEmail: false`, Emelia emails the link to `email` and the link does not expire. A second request for the same address is then refused with `LINKEDIN_INVITATION_PENDING`.

**Errors.** `AUTH_INVALID_EMAIL`, `INVALID_PARAMETERS`, `LINKEDIN_ACCOUNT_NOT_FOUND` (unknown `authId`), `LINKEDIN_ACCOUNT_LIMIT_REACHED` (no free LinkedIn slot).

## Authentication

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

## Request body

Content type: `application/json`, required.

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string | yes | Address of the person connecting the account. It identifies the invitation; with `sendEmail: false` nothing is sent to it. |
| `sendEmail` | boolean | no | `false` to get the link without sending any email. Default: `true`. |
| `authId` | string | no | Id of the account to reconnect. |

```json
{
  "email": "claire.fontaine@nexora.io",
  "sendEmail": false
}
```

## Example request

**cURL**

```bash
curl -X POST "https://api.emelia.io/linkedin/invitations" \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "claire.fontaine@nexora.io",
  "sendEmail": false
}'
```

**JavaScript**

```js
const response = await fetch('https://api.emelia.io/linkedin/invitations', {
  method: 'POST',
  headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "email": "claire.fontaine@nexora.io",
    "sendEmail": false
  }),
});
console.log(await response.json());
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.emelia.io/linkedin/invitations",
    headers={"Authorization": os.environ["EMELIA_API_KEY"]},
    json={
        "email": "claire.fontaine@nexora.io",
        "sendEmail": False
    },
)
print(response.json())
```

## Responses

### 200: Link created.

| Field | Type | Required | Description |
|---|---|---|---|
| `success` | boolean | no |  |
| `token` | string | no |  |
| `url` | string | no |  |
| `expiresAt` | string (date-time) | no |  |

```json
{
  "success": true,
  "token": "7b1f0c52-4d7e-4a3a-9a55-0f3ad1c2b8e1",
  "url": "https://app.emelia.io/linkedin-connect/7b1f0c52-4d7e-4a3a-9a55-0f3ad1c2b8e1",
  "expiresAt": "2026-11-11T10:00:00.000Z"
}
```

### 400: Invalid request or no free LinkedIn slot.

| 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`, `EMAIL_PROVIDER_NOT_FOUND`, `TRACKING_DOMAIN_MISSING`, `TRACKING_DOMAIN_INVALID`, `TRACKING_DOMAIN_TAKEN`, `TRACKING_DOMAIN_NOT_FOUND`, `TRACKING_DOMAIN_DISABLED`, `TRACKING_DOMAIN_PROVIDERS_MISSING`, `INVALID_STEP_ID`, `INVALID_VERSION_ID`, `DUPLICATE_STEP_ID` | 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`, `EMAIL_PROVIDER_NOT_FOUND`, `TRACKING_DOMAIN_MISSING`, `TRACKING_DOMAIN_INVALID`, `TRACKING_DOMAIN_TAKEN`, `TRACKING_DOMAIN_NOT_FOUND`, `TRACKING_DOMAIN_DISABLED`, `TRACKING_DOMAIN_PROVIDERS_MISSING`, `INVALID_STEP_ID`, `INVALID_VERSION_ID`, `DUPLICATE_STEP_ID` | 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"
}
```
