# Create a webhook

Source: https://docs.emelia.io/docs/webhooks/create-webhook

Guide: [Create a webhook](https://docs.emelia.io/docs/webhooks.md#create-a-webhook)

`POST https://api.emelia.io/webhook`

Registers a URL on an advanced (multichannel) campaign, or on all of them. The URL then receives a JSON `POST` each time one of the selected events happens. The events and their bodies are described in the events of this section.

**One campaign.** Pass its id in `campaignId`. `webhookId` in the response is the id of the registration. Registering the same URL again on the same campaign adds a second registration, and the URL then receives everything twice: to change the events, delete the URL first.

**All campaigns.** Pass `ALL_CAMPAIGNS` in `campaignId`. The URL is added to every campaign that is not `FINISHED` or `ARCHIVED` and does not have it yet, and it is kept for the campaigns you create later. A campaign that already has the URL keeps its events. `webhookId` is then the id of your account, not of a registration: read the ids with `GET /webhook`.

**Pitfalls.** Always send `type: advanced`. Without it, the route works on legacy email campaigns: a campaign id answers 404, and `ALL_CAMPAIGNS` answers `success: true` while no advanced campaign gets the URL. Event names are not validated: a misspelled name is accepted and never fires.

**Errors.** 400 `INVALID_PARAMETERS` when a field is missing, `events` is empty or `hookUrl` is not a URL. 400 without code when `campaignId` is not a valid id. 404 `CAMPAIGN_NOT_FOUND` when the campaign does not exist on your account.

## Authentication

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

## Request body

Content type: `application/json`, required.

| Field | Type | Required | Description |
|---|---|---|---|
| `hookUrl` | string | yes | The URL that receives the events. Use a public HTTPS URL that is hard to guess: deliveries are not signed. |
| `campaignId` | string | yes | Id of an advanced campaign, or `ALL_CAMPAIGNS`. |
| `type` | enum: `advanced` | yes | Always `advanced`. Without it, the route works on legacy campaigns instead (see the description). |
| `events` | array of enum: `EMAIL_SENT`, `OPENED`, `CLICKED`, `MAIL_REPLIED`, `BOUNCED`, `UNSUBSCRIBED`, `VISITED`, `INVITED`, `ACCEPTED`, `MESSAGE_SENT`, `INMAIL_SENT`, `LIKED`, `LINKEDIN_REPLIED`, `RE_REPLY_EMAIL`, `RE_REPLY_LINKEDIN`, `WHATSAPP_MESSAGE_SENT`, `WHATSAPP_REPLIED`, `CAMPAIGN_ENDING` | yes | The events to receive. |

```json
{
  "hookUrl": "https://hooks.acme.com/emelia/9f2c4e1a7b",
  "campaignId": "6651f0c2a1b2c3d4e5f60718",
  "type": "advanced",
  "events": [
    "MAIL_REPLIED",
    "LINKEDIN_REPLIED"
  ]
}
```

## Example request

**cURL**

```bash
curl -X POST "https://api.emelia.io/webhook" \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "hookUrl": "https://hooks.acme.com/emelia/9f2c4e1a7b",
  "campaignId": "6651f0c2a1b2c3d4e5f60718",
  "type": "advanced",
  "events": [
    "MAIL_REPLIED",
    "LINKEDIN_REPLIED"
  ]
}'
```

**JavaScript**

```js
const response = await fetch('https://api.emelia.io/webhook', {
  method: 'POST',
  headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "hookUrl": "https://hooks.acme.com/emelia/9f2c4e1a7b",
    "campaignId": "6651f0c2a1b2c3d4e5f60718",
    "type": "advanced",
    "events": [
      "MAIL_REPLIED",
      "LINKEDIN_REPLIED"
    ]
  }),
});
console.log(await response.json());
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.emelia.io/webhook",
    headers={"Authorization": os.environ["EMELIA_API_KEY"]},
    json={
        "hookUrl": "https://hooks.acme.com/emelia/9f2c4e1a7b",
        "campaignId": "6651f0c2a1b2c3d4e5f60718",
        "type": "advanced",
        "events": [
            "MAIL_REPLIED",
            "LINKEDIN_REPLIED"
        ]
    },
)
print(response.json())
```

## Responses

### 200: Webhook registered.

| Field | Type | Required | Description |
|---|---|---|---|
| `success` | boolean | yes |  |
| `webhookId` | string | yes | Id of the registration. With `ALL_CAMPAIGNS`, the id of your account. |

```json
{
  "success": true,
  "webhookId": "6702c1d4e5f6a7b8c9d0e1f2"
}
```

### 400: Missing or invalid field.

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

### 404: Campaign not found on your 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`, `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": {}
}
```
