# SmartChat webhooks

# Webhooks

URL: https://docs.smartchat.marketing/en/webhooks/overview/

> SmartChat calls your web address when something happens. Headers, payload, delivery rules and how to respond.

A webhook lets SmartChat tell your software that something happened, for example a new contact or a message that was delivered. You register an **endpoint** (an https address) and choose the **events** you want. SmartChat sends each event to the endpoint as a `POST` with a JSON body.

Set one up in [Set up a webhook](https://docs.smartchat.marketing/en/guides/set-up-a-webhook/). The list of events is on the [events page](https://docs.smartchat.marketing/en/webhooks/events/).

## What you receive

Headers:

| Header | Meaning |
|---|---|
| `SmartChat-Signature` | `t=<unix seconds>,v1=<hex>`. Proves the delivery is from SmartChat. [How to check it](https://docs.smartchat.marketing/en/webhooks/signature-verification/). |
| `SmartChat-Event-Id` | ID of the event (`evt_...`). Same for every endpoint. |
| `SmartChat-Event-Type` | The event name, for example `contact.created`. |
| `SmartChat-Delivery-Id` | ID of this delivery (`whd_...`). |
| `SmartChat-Delivery-Attempt` | Number of this try, starting at 1. |
| `User-Agent` | `SmartChat-Webhooks/2` |
| `Content-Type` | `application/json` |

Body:

```json
{
  "id": "evt_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "object": "event",
  "type": "contact.opted_out",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:07.412Z",
  "sequence": 128734,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": { "object": "contact", "id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234", "optin_whatsapp": "opted_out" },
    "previous_attributes": { "optin_whatsapp": "confirmed" }
  }
}
```

| Field | Meaning |
|---|---|
| `id` | Event ID. Use it to find duplicates. |
| `type` | Event name. |
| `created_at` | When the event happened, with milliseconds, in UTC. |
| `sequence` | A counter to sort events. |
| `request_id` | The ID of the API call that caused the event, if there was one. |
| `data.object` | The object the event is about. |
| `data.previous_attributes` | For changes: the old values of the changed fields. |

The body is at most 256 KB. If it would be larger, `data.object` is cut to `object` and `id` and the body gets `"truncated": true`. Read the full object with the API.

## What you must do

1. **Check the signature** with the raw body. [Code in shell, Python and TypeScript](https://docs.smartchat.marketing/en/webhooks/signature-verification/).
2. **Answer fast** with any 2xx status. SmartChat waits 10 seconds. Do the real work after you answered.
3. **Expect duplicates.** Delivery is *at least once*. Keep the `id` of events you handled and skip repeats.
4. **Do not rely on order.** Events can arrive out of order. Use `sequence` or `created_at` to sort.
5. **Use https.** Only https on port 443 is allowed, without a user name or password in the address, and without redirects. Internal and private addresses are refused ([`webhook_url_invalid`](https://docs.smartchat.marketing/en/errors/#webhook_url_invalid)). SmartChat checks the address when you save it and again at every connection.

An account can have up to 20 endpoints. The same address twice in one account is a conflict. Endpoints belong to the account, not to a key: revoking the key that created an endpoint does not stop it.

## Failures

If your endpoint does not answer with 2xx, SmartChat tries again later. The schedule, the health states of an endpoint and what happens at the end are on [Retries and health](https://docs.smartchat.marketing/en/webhooks/retries/).


---

# Webhook events

URL: https://docs.smartchat.marketing/en/webhooks/events/

> Every event with its meaning, the permission needed to subscribe and an example body. Events that are not sent yet are marked.

Event names have the form `resource.event` in the past tense. To subscribe an endpoint to an event your key needs the permission shown in the table. Events marked **coming soon** are listed so that you can plan; an endpoint cannot subscribe to them yet and the API answers with [`validation_failed`](https://docs.smartchat.marketing/en/errors/#validation_failed).

The status in the table is the truth for today: **available** means that SmartChat sends the event now, **coming soon** means that nothing sends it yet.

Every body has the same envelope. See [Webhooks](https://docs.smartchat.marketing/en/webhooks/overview/). Some events add fields inside `data`, next to `object`: `contact.updated` has `previous_attributes` and `changed_fields`, `contact.tag_removed` has `tag`, `contact.opted_out` has `reason`, and `message.failed` has `failure_code`.

When you create an endpoint, `["*"]` means all available events (never `webhook.test`). `webhook.test` needs only the permission `webhooks:write`.

| Event | When it happens | Permission to subscribe | Status |
|---|---|---|---|
| [`contact.created`](#contact-created) | A contact was created. | `contacts:read` | available |
| [`contact.updated`](#contact-updated) | Name, email, phone number or tags of a contact changed (see changed_fields). | `contacts:read` | coming soon (cannot be subscribed to yet) |
| [`contact.deleted`](#contact-deleted) | A contact was deleted. Only id and deleted_at are sent. | `contacts:read` | coming soon (cannot be subscribed to yet) |
| [`contact.tag_removed`](#contact-tag_removed) | A tag was removed from a contact (see tag). | `contacts:read` | coming soon (cannot be subscribed to yet) |
| [`contact.opted_in`](#contact-opted_in) | A contact confirmed the WhatsApp opt-in. | `contacts:read` | coming soon (cannot be subscribed to yet) |
| [`contact.opted_out`](#contact-opted_out) | A contact unsubscribed (see reason). | `contacts:read` | coming soon (cannot be subscribed to yet) |
| [`conversation.created`](#conversation-created) | A new conversation started. | `inbox:read` | available |
| [`conversation.handover_requested`](#conversation-handover_requested) | The AI handed a conversation over to a human. | `inbox:read` | coming soon (cannot be subscribed to yet) |
| [`message.received`](#message-received) | An incoming message arrived. | `inbox:read` | available |
| [`message.sent`](#message-sent) | An outgoing message was sent to WhatsApp. | `inbox:read` | coming soon (cannot be subscribed to yet) |
| [`message.delivered`](#message-delivered) | An outgoing message was delivered. | `inbox:read` | coming soon (cannot be subscribed to yet) |
| [`message.read`](#message-read) | An outgoing message was read. | `inbox:read` | coming soon (cannot be subscribed to yet) |
| [`message.failed`](#message-failed) | An outgoing message failed (see failure_code). | `inbox:read` | coming soon (cannot be subscribed to yet) |
| [`send.completed`](#send-completed) | A send created through the API completed. | `sends:read` | coming soon (cannot be subscribed to yet) |
| [`send.failed`](#send-failed) | A send created through the API failed. | `sends:read` | coming soon (cannot be subscribed to yet) |
| [`template.status_changed`](#template-status_changed) | The WhatsApp approval status of a template changed. | `templates:read` | coming soon (cannot be subscribed to yet) |
| [`newsletter.scheduled`](#newsletter-scheduled) | A newsletter was scheduled. | `newsletters:read` | coming soon (cannot be subscribed to yet) |
| [`newsletter.started`](#newsletter-started) | Sending of a newsletter started. | `newsletters:read` | coming soon (cannot be subscribed to yet) |
| [`newsletter.completed`](#newsletter-completed) | Sending of a newsletter completed. | `newsletters:read` | coming soon (cannot be subscribed to yet) |
| [`newsletter.failed`](#newsletter-failed) | Sending of a newsletter failed. | `newsletters:read` | coming soon (cannot be subscribed to yet) |
| [`automation.run_failed`](#automation-run_failed) | An automation run failed. | `automations:read` | coming soon (cannot be subscribed to yet) |
| [`account.limit_reached`](#account-limit_reached) | The monthly message quota or the credits of the account are used up. Not about API keys: a key has no usage limit. | `account:read` | coming soon (cannot be subscribed to yet) |
| [`account.limit_warning`](#account-limit_warning) | The monthly message quota or the credits of the account are at 80 percent. Not about API keys: a key has no usage limit. | `account:read` | coming soon (cannot be subscribed to yet) |
| [`webhook.test`](#webhook-test) | Test ping sent on request to an endpoint. | `webhooks:write` | available |

### contact.created


A contact was created. Object in `data.object`: `contact`. Permission to subscribe: `contacts:read`. Status: available.

```json
{
  "id": "evt_000000000000000000000000000000e0",
  "object": "event",
  "type": "contact.created",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:07.412Z",
  "sequence": 128734,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "contact",
      "id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "name": "Mia Muster",
      "phone": "+4915112345678",
      "email": "mia@example.com",
      "tags": [
        "regular customer"
      ],
      "optin_whatsapp": "none",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### contact.updated


Name, email, phone number or tags of a contact changed (see changed_fields). Object in `data.object`: `contact`. Permission to subscribe: `contacts:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000e1",
  "object": "event",
  "type": "contact.updated",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:08.412Z",
  "sequence": 128735,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "contact",
      "id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "name": "Mia Muster-Berg",
      "phone": "+4915112345678",
      "email": "mia@example.com",
      "tags": [
        "regular customer"
      ],
      "optin_whatsapp": "none",
      "created_at": "2026-10-25T09:41:07Z"
    },
    "previous_attributes": {
      "name": "Mia Muster"
    },
    "changed_fields": [
      "name"
    ]
  }
}
```

### contact.deleted


A contact was deleted. Only id and deleted_at are sent. Object in `data.object`: `contact`. Permission to subscribe: `contacts:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000e2",
  "object": "event",
  "type": "contact.deleted",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:09.412Z",
  "sequence": 128736,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "contact",
      "id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "deleted_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### contact.tag_removed


A tag was removed from a contact (see tag). Object in `data.object`: `contact`. Permission to subscribe: `contacts:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000e3",
  "object": "event",
  "type": "contact.tag_removed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:10.412Z",
  "sequence": 128737,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "contact",
      "id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "name": "Mia Muster",
      "phone": "+4915112345678",
      "email": "mia@example.com",
      "tags": [],
      "optin_whatsapp": "none",
      "created_at": "2026-10-25T09:41:07Z"
    },
    "tag": "regular customer"
  }
}
```

### contact.opted_in


A contact confirmed the WhatsApp opt-in. Object in `data.object`: `contact`. Permission to subscribe: `contacts:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000e4",
  "object": "event",
  "type": "contact.opted_in",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:11.412Z",
  "sequence": 128738,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "contact",
      "id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "name": "Mia Muster",
      "phone": "+4915112345678",
      "email": "mia@example.com",
      "tags": [
        "regular customer"
      ],
      "optin_whatsapp": "confirmed",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### contact.opted_out


A contact unsubscribed (see reason). Object in `data.object`: `contact`. Permission to subscribe: `contacts:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000e5",
  "object": "event",
  "type": "contact.opted_out",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:12.412Z",
  "sequence": 128739,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "contact",
      "id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "name": "Mia Muster",
      "phone": "+4915112345678",
      "email": "mia@example.com",
      "tags": [
        "regular customer"
      ],
      "optin_whatsapp": "opted_out",
      "created_at": "2026-10-25T09:41:07Z"
    },
    "previous_attributes": {
      "optin_whatsapp": "confirmed"
    },
    "reason": "stop_keyword"
  }
}
```

### conversation.created


A new conversation started. Object in `data.object`: `conversation`. Permission to subscribe: `inbox:read`. Status: available.

```json
{
  "id": "evt_000000000000000000000000000000e6",
  "object": "event",
  "type": "conversation.created",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:13.412Z",
  "sequence": 128740,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "conversation",
      "id": "cnv_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "contact_id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "channel": "whatsapp",
      "window_expires_at": "2026-10-26T09:41:07Z",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### conversation.handover_requested


The AI handed a conversation over to a human. Object in `data.object`: `conversation`. Permission to subscribe: `inbox:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000e7",
  "object": "event",
  "type": "conversation.handover_requested",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:14.412Z",
  "sequence": 128741,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "conversation",
      "id": "cnv_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "contact_id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "channel": "whatsapp",
      "window_expires_at": "2026-10-26T09:41:07Z",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### message.received


An incoming message arrived. Object in `data.object`: `message`. Permission to subscribe: `inbox:read`. Status: available.

```json
{
  "id": "evt_000000000000000000000000000000e8",
  "object": "event",
  "type": "message.received",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:15.412Z",
  "sequence": 128742,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "message",
      "id": "msg_0f8c1a2e4b7d4c3e9a5b6d7e8f909999",
      "conversation_id": "cnv_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "contact_id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "direction": "inbound",
      "text": "Hello, do you have a table for two?",
      "status": "received",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### message.sent


An outgoing message was sent to WhatsApp. Object in `data.object`: `message`. Permission to subscribe: `inbox:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000e9",
  "object": "event",
  "type": "message.sent",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:16.412Z",
  "sequence": 128743,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "message",
      "id": "msg_0f8c1a2e4b7d4c3e9a5b6d7e8f909999",
      "conversation_id": "cnv_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "contact_id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "direction": "outbound",
      "text": "Your table is booked for 7 pm.",
      "status": "sent",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### message.delivered


An outgoing message was delivered. Object in `data.object`: `message`. Permission to subscribe: `inbox:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000ea",
  "object": "event",
  "type": "message.delivered",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:17.412Z",
  "sequence": 128744,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "message",
      "id": "msg_0f8c1a2e4b7d4c3e9a5b6d7e8f909999",
      "conversation_id": "cnv_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "contact_id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "direction": "outbound",
      "text": "Your table is booked for 7 pm.",
      "status": "delivered",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### message.read


An outgoing message was read. Object in `data.object`: `message`. Permission to subscribe: `inbox:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000eb",
  "object": "event",
  "type": "message.read",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:18.412Z",
  "sequence": 128745,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "message",
      "id": "msg_0f8c1a2e4b7d4c3e9a5b6d7e8f909999",
      "conversation_id": "cnv_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "contact_id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "direction": "outbound",
      "text": "Your table is booked for 7 pm.",
      "status": "read",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### message.failed


An outgoing message failed (see failure_code). Object in `data.object`: `message`. Permission to subscribe: `inbox:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000ec",
  "object": "event",
  "type": "message.failed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:19.412Z",
  "sequence": 128746,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "message",
      "id": "msg_0f8c1a2e4b7d4c3e9a5b6d7e8f909999",
      "conversation_id": "cnv_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "contact_id": "con_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "direction": "outbound",
      "text": "Your table is booked for 7 pm.",
      "status": "failed",
      "created_at": "2026-10-25T09:41:07Z"
    },
    "failure_code": "window_closed"
  }
}
```

### send.completed


A send created through the API completed. Object in `data.object`: `send`. Permission to subscribe: `sends:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000ed",
  "object": "event",
  "type": "send.completed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:20.412Z",
  "sequence": 128747,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "send",
      "id": "snd_00000000000000000000000000000051",
      "status": "completed",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### send.failed


A send created through the API failed. Object in `data.object`: `send`. Permission to subscribe: `sends:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000ee",
  "object": "event",
  "type": "send.failed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:21.412Z",
  "sequence": 128748,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "send",
      "id": "snd_00000000000000000000000000000052",
      "status": "failed",
      "created_at": "2026-10-25T09:41:07Z"
    }
  }
}
```

### template.status_changed


The WhatsApp approval status of a template changed. Object in `data.object`: `template`. Permission to subscribe: `templates:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000ef",
  "object": "event",
  "type": "template.status_changed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:22.412Z",
  "sequence": 128749,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "template",
      "id": "tpl_00000000000000000000000000000061",
      "status": "approved"
    }
  }
}
```

### newsletter.scheduled


A newsletter was scheduled. Object in `data.object`: `newsletter`. Permission to subscribe: `newsletters:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000f0",
  "object": "event",
  "type": "newsletter.scheduled",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:23.412Z",
  "sequence": 128750,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "newsletter",
      "id": "nwl_00000000000000000000000000000071",
      "status": "scheduled"
    }
  }
}
```

### newsletter.started


Sending of a newsletter started. Object in `data.object`: `newsletter`. Permission to subscribe: `newsletters:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000f1",
  "object": "event",
  "type": "newsletter.started",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:24.412Z",
  "sequence": 128751,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "newsletter",
      "id": "nwl_00000000000000000000000000000072",
      "status": "sending"
    }
  }
}
```

### newsletter.completed


Sending of a newsletter completed. Object in `data.object`: `newsletter`. Permission to subscribe: `newsletters:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000f2",
  "object": "event",
  "type": "newsletter.completed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:25.412Z",
  "sequence": 128752,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "newsletter",
      "id": "nwl_00000000000000000000000000000073",
      "status": "completed"
    }
  }
}
```

### newsletter.failed


Sending of a newsletter failed. Object in `data.object`: `newsletter`. Permission to subscribe: `newsletters:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000f3",
  "object": "event",
  "type": "newsletter.failed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:26.412Z",
  "sequence": 128753,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "newsletter",
      "id": "nwl_00000000000000000000000000000074",
      "status": "failed"
    }
  }
}
```

### automation.run_failed


An automation run failed. Object in `data.object`: `automation_run`. Permission to subscribe: `automations:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000f4",
  "object": "event",
  "type": "automation.run_failed",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:27.412Z",
  "sequence": 128754,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "automation_run",
      "id": "run_00000000000000000000000000000081",
      "automation_id": "aut_00000000000000000000000000000082",
      "status": "failed"
    }
  }
}
```

### account.limit_reached


The monthly message quota or the credits of the account are used up. Not about API keys: a key has no usage limit. Object in `data.object`: `account`. Permission to subscribe: `account:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000f5",
  "object": "event",
  "type": "account.limit_reached",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:28.412Z",
  "sequence": 128755,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "account",
      "id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234"
    }
  }
}
```

### account.limit_warning


The monthly message quota or the credits of the account are at 80 percent. Not about API keys: a key has no usage limit. Object in `data.object`: `account`. Permission to subscribe: `account:read`. Status: coming soon. It is listed so that you can plan, but an endpoint cannot subscribe to it yet. The example shows the planned shape and can still change.

```json
{
  "id": "evt_000000000000000000000000000000f6",
  "object": "event",
  "type": "account.limit_warning",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:29.412Z",
  "sequence": 128756,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "account",
      "id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234"
    }
  }
}
```

### webhook.test


Test ping sent on request to an endpoint. Object in `data.object`: `webhook_test`. Permission to subscribe: `webhooks:write`. Status: available.

```json
{
  "id": "evt_000000000000000000000000000000f7",
  "object": "event",
  "type": "webhook.test",
  "api_version": "2026-10-25",
  "created_at": "2026-10-25T09:41:30.412Z",
  "sequence": 128757,
  "account_id": "acc_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "livemode": true,
  "request_id": "req_01JAB9C3X7K2M4N5P6Q7R8S9T0",
  "data": {
    "object": {
      "object": "webhook_test",
      "id": "whe_00000000000000000000000000000091"
    }
  }
}
```


---

# Verify a signature

URL: https://docs.smartchat.marketing/en/webhooks/signature-verification/

> Copy-paste code in shell, Python and TypeScript to check that a webhook delivery comes from SmartChat. Includes replay protection and secret rotation.

Anyone who knows your webhook address can send a request to it. The signature proves that a delivery is from SmartChat and was not changed.

## How the signature works

- Every delivery has the header `SmartChat-Signature: t=<unix seconds>,v1=<hex>`.
- The signed text is the timestamp, a dot, and the **raw request body**: `t + "." + body`.
- The signature is HMAC-SHA256 of that text, written as lower case hex. The key is the **secret of the endpoint**, exactly as shown (it starts with `whsec_`).
- During a secret rotation the header has **two** `v1=` values. One must match.

## Check these things, in this order

1. The header exists and has `t` and at least one `v1`.
2. The timestamp is within 5 minutes of your clock. This stops someone from sending an old delivery again (a replay).
3. The signature matches. Compare in constant time.
4. You have not handled this event `id` before.

Use the **raw body**. If you parse the JSON and write it again, the bytes change and the check fails.

## Code

Copy the code for your language. It is the same code that SmartChat tests with its own deliveries. Each function returns `true` (or prints `valid`) if the delivery is valid. Pass the **raw body** and the secret. During a rotation you can pass both secrets (the Python and TypeScript functions take a list).

**Shell (openssl)**

```bash
#!/bin/sh
# Verify a SmartChat webhook with curl-friendly shell tools (needs openssl).
# Usage: ./verify.sh <secret> "<SmartChat-Signature header>" <body-file>
# The body file must contain the RAW request body exactly as received (save it with: curl ... --data-binary @- or from your server log).
secret="$1"; header="$2"; file="$3"
timestamp=$(printf '%s' "$header" | tr ',' '\n' | sed -n 's/^t=//p' | head -n 1)
case "$timestamp" in ''|*[!0-9]*) echo invalid; exit 1;; esac
now=$(date +%s)
diff=$((now - timestamp)); [ "$diff" -lt 0 ] && diff=$((0 - diff))
[ "$diff" -gt 300 ] && { echo invalid; exit 1; }   # replay protection: more than 5 minutes
expected=$( { printf '%s.' "$timestamp"; cat "$file"; } | openssl dgst -sha256 -hmac "$secret" | sed 's/^.*= //')
for sig in $(printf '%s' "$header" | tr ',' '\n' | sed -n 's/^v1=//p'); do
  if [ "$sig" = "$expected" ]; then echo valid; exit 0; fi
done
echo invalid; exit 1
```

**Python**

```python
# Verify a SmartChat webhook in Python 3 (standard library only).
# Pass the RAW request body (bytes, exactly as received, before any JSON parsing).
import hashlib
import hmac
import sys
import time


def verify_smartchat_signature(raw_body: bytes, header: str, secrets: list, tolerance_seconds: int = 300) -> bool:
    if not header:
        return False
    timestamp = ""
    signatures = []
    for part in header.split(","):
        key, _, value = part.partition("=")
        if key == "t":
            timestamp = value
        elif key == "v1" and value:
            signatures.append(value)
    if not timestamp.isdigit() or not signatures:
        return False
    if abs(time.time() - int(timestamp)) > tolerance_seconds:  # replay protection
        return False
    for secret in secrets:
        signed = timestamp.encode() + b"." + raw_body
        expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
        if any(hmac.compare_digest(sig, expected) for sig in signatures):
            return True
    return False


if __name__ == "__main__" and len(sys.argv) >= 4:
    # python3 verify.py <secret> <header> <body-file>
    with open(sys.argv[3], "rb") as f:
        ok = verify_smartchat_signature(f.read(), sys.argv[2], [sys.argv[1]])
    print("valid" if ok else "invalid")
    sys.exit(0 if ok else 1)
```

**TypeScript**

```typescript
// Verify a SmartChat webhook in TypeScript (Node 18+). No dependencies.
// Usage in your server: pass the RAW request body (a string or Buffer, exactly as received, before any JSON parsing).
import { createHmac, timingSafeEqual } from "node:crypto";
import { readFileSync } from "node:fs";

export function verifySmartChatSignature(rawBody: string | Buffer, header: string | undefined, secrets: string[], toleranceSeconds = 300): boolean {
  if (!header) return false;
  let timestamp = "";
  const signatures: string[] = [];
  for (const part of header.split(",")) {
    const [key, value] = part.split("=", 2);
    if (key === "t") timestamp = value ?? "";
    if (key === "v1" && value) signatures.push(value);
  }
  if (!/^\d+$/.test(timestamp) || signatures.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false; // replay protection
  const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
  for (const secret of secrets) {
    const expected = createHmac("sha256", secret).update(`${timestamp}.${body}`).digest("hex");
    for (const sig of signatures) {
      if (sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return true;
    }
  }
  return false;
}

// Command line use: node --import tsx verify.ts <secret> <header> <body-file>
if (process.argv[1]?.endsWith("verify.ts") && process.argv.length >= 5) {
  const ok = verifySmartChatSignature(readFileSync(process.argv[4]!, "utf8"), process.argv[3], [process.argv[2]!]);
  console.log(ok ? "valid" : "invalid");
  process.exit(ok ? 0 : 1);
}
```

## Get the raw body in your web framework

The most common mistake is to let the framework parse the JSON first. Then you no longer have the original bytes. Ask the framework for the raw body, check the signature, and only then parse. These two examples use the function `verifySmartChatSignature` from the TypeScript code above. They need a web framework that is not installed here, so they are shown as text and are not run by the SmartChat tests.

**Express**

```typescript
import express from "express";

const app = express();

// express.raw keeps the body as a Buffer. Do NOT use express.json() on this route.
app.post("/hooks/smartchat", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  const ok = verifySmartChatSignature(raw, req.header("SmartChat-Signature") ?? "", [process.env.SMARTCHAT_WEBHOOK_SECRET!]);
  if (!ok) return res.status(400).send("invalid signature");
  const event = JSON.parse(raw);
  // Handle the event. Answer quickly with 2xx.
  res.sendStatus(200);
});
```

**Next.js (route handler)**

```typescript
export async function POST(request: Request) {
  const raw = await request.text(); // the raw body, not request.json()
  const ok = verifySmartChatSignature(raw, request.headers.get("SmartChat-Signature") ?? "", [process.env.SMARTCHAT_WEBHOOK_SECRET!]);
  if (!ok) return new Response("invalid signature", { status: 400 });
  const event = JSON.parse(raw);
  // Handle the event.
  return new Response(null, { status: 200 });
}
```

## Rotating the secret

Rotate the secret if it may have leaked. The rotation call creates a new secret and returns it once. For a time you choose (`grace_hours`, 0 to 72, default 24) deliveries are signed with **both** secrets, so you can switch without losing events. After that only the new secret is used. Update your code to check with the new secret, then wait for the grace time to end.

## Test your code

Use the test call of the endpoint. It sends a `webhook.test` event with a real signature. See [Webhook testing](https://docs.smartchat.marketing/en/webhooks/testing/).


---

# Retries and health

URL: https://docs.smartchat.marketing/en/webhooks/retries/

> What happens when your endpoint does not answer. The retry schedule, how an endpoint is paused and disabled, and how to get missed events back.

## What counts as success

Any 2xx status within 10 seconds. Everything else is a failure: a timeout, a connection error, a status of 3xx, 4xx or 5xx. Redirects are never followed, so a 3xx answer is a failure.

- **410 Gone** disables the endpoint at once. Use it if you want SmartChat to stop.
- On 429 and 5xx SmartChat respects the `Retry-After` header (at most 24 hours).

## Retry schedule

A failed delivery is tried again. There are up to 9 attempts, and no later than 72 hours after the event (the last attempt is moved forward to that point). A random change of up to 20 percent is added to each wait so that many deliveries do not arrive at the same moment. The header `SmartChat-Delivery-Attempt` tells you which attempt you got.

| Attempt | Wait before the attempt |
|---|---|
| 1 | immediately |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 1 hour |
| 6 | 4 hours |
| 7 | 12 hours |
| 8 | 1 day |
| 9 | 2 days |

After the last attempt the delivery is `failed`. Failed deliveries stay in the delivery log for 30 days. You can send them again by hand.

## Health of an endpoint

An endpoint is never switched off after a single failed delivery.

| State | What it means | How it changes |
|---|---|---|
| `active` | Receives events. | 5 failed attempts in a row: `degraded`. |
| `degraded` | Still receives events. The state is shown in the app and in the endpoint (`status`). SmartChat does not send a warning mail yet, so check the status from time to time. | A success: `active`. At least 3 days without a success and at least 20 attempts in a row: `paused`. |
| `paused` | Receives no events. Deliveries that are waiting when the endpoint is paused fail with `endpoint_paused` and are not sent later by themselves. | 7 days without a success: `disabled`. The enable call makes it `active` again. |
| `disabled` | Receives no events. | A 410 answer disables an endpoint at once. The enable call makes it `active` again. |

The test call does not change the health of an endpoint.

**Events in a pause come back only by replay.** While an endpoint is `paused` or `disabled`, its events are not queued for it and waiting deliveries are marked failed. When you turn it on again, send `replay_since` in the enable call (the time of your last good delivery, at most 7 days back and 10,000 events), or call replay afterwards. Without a replay, those events are lost for this endpoint.

## Get missed events back

- **Send one delivery again:** the redeliver call on a delivery.
- **Turn an endpoint on again:** the enable call (for `paused` and `disabled` endpoints). You can ask it to replay events from a time in the same call.
- **Replay events:** the replay call sends events of a time range again (at most 7 days back and 10,000 events; it needs an `Idempotency-Key`).

All calls are in [Webhook endpoints API](https://docs.smartchat.marketing/en/webhooks/endpoints-api/).


---

# Webhook testing

URL: https://docs.smartchat.marketing/en/webhooks/testing/

> Test a webhook endpoint without waiting for a real event, and find out why a delivery failed.

## Send a test event

The test call sends a `webhook.test` event to your endpoint, signed like a real one, and waits for the answer. The answer of the API contains the HTTP status your server gave and how long it took.

**curl**

```bash
curl -X POST "https://api.smartchat.marketing/v1/webhook-endpoints/whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234/test" \
  -H "Authorization: Bearer $SMARTCHAT_API_KEY"
```

**Python**

```python
import os
import requests

response = requests.post(
    "https://api.smartchat.marketing/v1/webhook-endpoints/whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234/test",
    headers={"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"]},
)
response.raise_for_status()
print(response.json())
```

**TypeScript**

```typescript
const response = await fetch("https://api.smartchat.marketing/v1/webhook-endpoints/whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234/test", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
  },
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

Response (200):


```json
{
  "object": "webhook_test_result",
  "endpoint_id": "whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
  "delivery_id": "whd_7a1b2c3d4e5f40718293a4b5c6d7e8f9",
  "delivered": true,
  "http_status": 200,
  "duration_ms": 182,
  "error": null
}
```

A test event does not change the health of the endpoint. The permission for the test event is `webhooks:write`.

## Test on your own computer

SmartChat only calls https addresses on port 443. To test with a program on your computer, use a tool that gives your local server a public https address, and register that address as the endpoint. Remove the endpoint when you are done.

## Find out why a delivery failed

The delivery log lists every try with its status, the HTTP status, a short cleaned error text and the duration.

**curl**

```bash
curl "https://api.smartchat.marketing/v1/webhook-endpoints/whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234/deliveries?status=failed&limit=20" \
  -H "Authorization: Bearer $SMARTCHAT_API_KEY"
```

**Python**

```python
import os
import requests

response = requests.get(
    "https://api.smartchat.marketing/v1/webhook-endpoints/whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234/deliveries",
    headers={"Authorization": "Bearer " + os.environ["SMARTCHAT_API_KEY"]},
    params={
        "status": "failed",
        "limit": "20",
    },
)
response.raise_for_status()
print(response.json())
```

**TypeScript**

```typescript
const response = await fetch("https://api.smartchat.marketing/v1/webhook-endpoints/whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234/deliveries?status=failed&limit=20", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.SMARTCHAT_API_KEY}`,
  },
});
if (!response.ok) throw new Error(await response.text());
console.log(await response.json());
```

Response (200):


```json
{
  "object": "list",
  "data": [
    {
      "object": "webhook_delivery",
      "id": "whd_7a1b2c3d4e5f40718293a4b5c6d7e8f9",
      "endpoint_id": "whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234",
      "event_id": "evt_0f8c1a2e4b7d4c3e9a5b6d7e8f905678",
      "type": "contact.created",
      "attempt": 1,
      "status": "delivered",
      "http_status": 200,
      "error": null,
      "duration_ms": 182,
      "next_attempt_at": null,
      "created_at": "2026-10-25T09:44:59Z",
      "finished_at": "2026-10-25T09:45:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null,
  "url": "/v1/webhook-endpoints/whe_0f8c1a2e4b7d4c3e9a5b6d7e8f901234/deliveries"
}
```

Typical reasons:

| You see | Reason |
|---|---|
| `http_status` 401 or 403 | Your server refused the request. Check the sign-in rules of your own server for this address. |
| `http_status` 404 | The path is wrong. |
| Timeout | Your server needed more than 10 seconds. Answer first, work later. |
| Signature check fails in your code | You used a parsed body instead of the raw body, or the wrong secret. See [Verify a signature](https://docs.smartchat.marketing/en/webhooks/signature-verification/). |


---

# Webhook endpoints API

URL: https://docs.smartchat.marketing/en/webhooks/endpoints-api/

> The calls to create, read, change and unsubscribe webhook endpoints, rotate secrets, test, replay and read deliveries.

> **Example values** The secrets (`whsec_...`) in the examples on this page are made-up example values. The real secret is shown **once**, in the answer to the call that creates or rotates an endpoint. Copy it then. It cannot be read again.

These calls manage your outgoing webhooks. Reading needs `webhooks:read`; changing needs `webhooks:write`. Removing an endpoint (unsubscribing) is one of the few removals that keys may do. It removes the endpoint and its delivery log.

- [Create webhook endpoint](https://docs.smartchat.marketing/en/api/reference/webhooks/create_webhook_endpoint/): `POST /v1/webhook-endpoints`
- [List webhook endpoints](https://docs.smartchat.marketing/en/api/reference/webhooks/list_webhook_endpoints/): `GET /v1/webhook-endpoints`
- [Get webhook endpoint](https://docs.smartchat.marketing/en/api/reference/webhooks/get_webhook_endpoint/): `GET /v1/webhook-endpoints/{endpoint_id}`
- [Update webhook endpoint](https://docs.smartchat.marketing/en/api/reference/webhooks/update_webhook_endpoint/): `PATCH /v1/webhook-endpoints/{endpoint_id}`
- [Unsubscribe webhook endpoint](https://docs.smartchat.marketing/en/api/reference/webhooks/unsubscribe_webhook_endpoint/): `DELETE /v1/webhook-endpoints/{endpoint_id}`
- [Rotate webhook secret](https://docs.smartchat.marketing/en/api/reference/webhooks/rotate_webhook_secret/): `POST /v1/webhook-endpoints/{endpoint_id}/rotate-secret`
- [Enable webhook endpoint](https://docs.smartchat.marketing/en/api/reference/webhooks/enable_webhook_endpoint/): `POST /v1/webhook-endpoints/{endpoint_id}/enable`
- [Test webhook endpoint](https://docs.smartchat.marketing/en/api/reference/webhooks/test_webhook_endpoint/): `POST /v1/webhook-endpoints/{endpoint_id}/test`
- [Replay webhook events](https://docs.smartchat.marketing/en/api/reference/webhooks/replay_webhook_events/): `POST /v1/webhook-endpoints/{endpoint_id}/replay`
- [List webhook deliveries](https://docs.smartchat.marketing/en/api/reference/webhooks/list_webhook_deliveries/): `GET /v1/webhook-endpoints/{endpoint_id}/deliveries`
- [Redeliver webhook delivery](https://docs.smartchat.marketing/en/api/reference/webhooks/redeliver_webhook_delivery/): `POST /v1/webhook-deliveries/{delivery_id}/redeliver`

The full description of each call is in the [API reference](https://docs.smartchat.marketing/en/api/reference/webhooks/).

## Test, enable, replay and redeliver

These four calls look alike but behave differently. The table says what must be true first, what each call does, what happens if you repeat it, and whether it takes an `Idempotency-Key`.

| Call | What must be true first | What it does | If you repeat it | Idempotency-Key |
|---|---|---|---|---|
| [Test](https://docs.smartchat.marketing/en/api/reference/webhooks/test_webhook_endpoint/) | The endpoint exists. Its state is not checked. | Sends one signed `webhook.test` event at once and waits for the answer. Writes one line in the delivery log. Does not change the health of the endpoint and is never retried. | Each call sends a new test event with a new delivery ID. A test delivery cannot be redelivered. | Not needed |
| [Enable](https://docs.smartchat.marketing/en/api/reference/webhooks/enable_webhook_endpoint/) | The endpoint exists. Its address must still be allowed (`webhook_url_invalid`) and must not be used by another endpoint that is not disabled (`conflict`). | Sets the state to `active`, clears the reason and resets the failure counter. Events are queued for the endpoint again from now on if it was paused or disabled. | Safe to repeat: the endpoint stays `active`. | Optional |
| [Enable](https://docs.smartchat.marketing/en/api/reference/webhooks/enable_webhook_endpoint/) with `replay_since` | As above. In addition: the time is not in the future and at most 7 days back (`validation_failed`), the key has the read permission for every event type of the endpoint (`insufficient_scope`), and no replay of this endpoint is still running (`conflict`). | As above, and also queues the events since that time for this endpoint (only the types it subscribes to, at most 10,000). The answer has `replay.queued` and `replay.truncated`. | Events that already wait for delivery are not queued twice. While an earlier replay is still running, the call answers `conflict`. | Optional |
| [Replay](https://docs.smartchat.marketing/en/api/reference/webhooks/replay_webhook_events/) | The endpoint is `active` or `degraded` (otherwise `conflict`: enable it first). `since` is not in the future and at most 7 days back (`validation_failed`). The key has the read permission for every event type of the endpoint (`insufficient_scope`). No replay of this endpoint is still running (`conflict`). | Queues the events since `since` for this endpoint (only the types it subscribes to, at most 10,000, events removed for data protection are skipped). Status 202; the deliveries are sent afterwards with the normal retry schedule. | The same Idempotency-Key returns the first answer. Events that already wait are not queued twice. While a replay is still running, a new one answers `conflict`. | Required |
| [Redeliver](https://docs.smartchat.marketing/en/api/reference/webhooks/redeliver_webhook_delivery/) | The delivery belongs to your account and comes from a real event (a test delivery answers `conflict`). The endpoint is `active` or `degraded` (otherwise `conflict`). The event is still in the 30 day log and was not removed for data protection (otherwise `conflict`). The key has the read permission for the event type. | Queues one new delivery of the same event to the same endpoint. Status 202, new delivery ID, state `pending`. | Without a key every call queues one more delivery. The same Idempotency-Key returns the first answer. | Optional |

"Not needed" means the call does not ask for the key. "Safe by nature" or "safe to repeat" is a separate fact: the end state is the same when you repeat the call. See [Idempotency](https://docs.smartchat.marketing/en/concepts/idempotency/).


---

# Inbound webhooks

URL: https://docs.smartchat.marketing/en/webhooks/inbound-webhooks/

> Start an automation from another app with a secret web address. How the address works, how to read it, and how duplicates are handled.

An automation can start when another app (Zapier, Make, n8n or your own software) calls a secret web address. This is the opposite direction of the webhooks on the other pages.

## The address

The address has the form `/public/automations/hook/<token>`. The token is secret. Anyone who knows the address can start the automation, so treat it like a password.

- The address is **not** under `/v1` and needs no API key.
- There is no required signature. The secret address is the protection.
- To read the address of an automation with a key, the key needs the permission `hooks:read_secret`. This permission is ticked on its own.

## Duplicates

If the same event arrives twice without an ID and with the same content within one to two hours, it is discarded and noted internally.

## Filters

A filter is set when the automation is created. It decides which events start the automation. See [Build an automation](https://docs.smartchat.marketing/en/guides/build-an-automation/).

