> ## Documentation Index
> Fetch the complete documentation index at: https://help.ciarem.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Deliveries and retries

> How Ciarem delivers events, what happens when your server fails, how to read the delivery log, and how to send an event again.

A **delivery** is one event headed to one endpoint. If three of your endpoints subscribe to `contact.created`, one new contact produces three deliveries, each tracked and retried on its own.

## What counts as delivered

Ciarem sends the event the moment it happens, usually within a few seconds, and waits up to **10 seconds** for your server to answer.

| Your server | Result |
| - | - |
| Answers with any `2xx` status | **Delivered.** Ciarem does not read the response body. |
| Answers with `3xx`, `4xx`, or `5xx` | **Failed.** Redirects are not followed. |
| Takes longer than 10 seconds | **Failed.** |
| Refuses the connection, has an invalid TLS certificate, or cannot be resolved | **Failed.** |

A failed delivery is retried automatically.

## Retries

After a failure, Ciarem waits and tries again, with a longer wait each time:

| Attempt | Wait after the previous failure |
| - | - |
| 2nd | 1 minute |
| 3rd | 5 minutes |
| 4th | 30 minutes |
| 5th | 2 hours |
| 6th and later | 8 hours each |

Retries continue until the delivery is **3 days old**. After that Ciarem gives up on it and marks it **Failed**. In practice that is about 14 attempts. Retries run once a minute, so an attempt can start up to a minute after its scheduled time.

Every attempt sends the **same body** with the **same event `id`**. Only the `Ciarem-Signature` header changes, because each attempt is signed with a fresh timestamp.

## Delivery statuses

| In the dashboard | In the API | Meaning |
| - | - | - |
| **Pending** | `pending` | Queued, not attempted yet, or queued to be sent again. |
| **Delivered** | `succeeded` | Your server answered `2xx`. |
| **Retrying** | `failed` | The last attempt failed. Another one is scheduled at `next_attempt_at`. |
| **Failed** | `exhausted` | Ciarem stopped trying: the delivery passed 3 days, or its endpoint was disabled. |

## When an endpoint keeps failing

If **every** delivery to an endpoint fails for **3 days in a row**, Ciarem turns the endpoint off. In the dashboard it shows **Disabled after repeated failures**; in the API, `enabled` is `false` and `disabled_reason` is `auto_failures`.

A single successful delivery resets the count, so an endpoint with occasional failures is never disabled.

While an endpoint is disabled:

* Nothing is sent to it, and events that happen in the meantime are **not** saved for later.
* Deliveries that were waiting for a retry are marked **Failed**.

To turn it back on, fix your server, then choose **Enable** in the endpoint's menu, or send `PATCH /v1/webhook-endpoints/{id}` with `{"enabled": true}`. Delivery resumes with new events from that point on. To recover what you missed, see [Catching up after downtime](#catching-up-after-downtime).

## Pausing an endpoint yourself

Choose **Disable** in the endpoint's menu, or send `{"enabled": false}`, to stop deliveries without deleting the endpoint. It behaves like the automatic case: nothing is sent while it is off, waiting retries are dropped, and enabling it later does not replay old events. `disabled_reason` is `user`.

Deleting an endpoint also deletes its delivery log.

## If your plan changes

Webhooks are part of API access. If your organization moves to a plan without it, or a trial ends without a subscription, Ciarem stops sending events immediately. Your endpoints are kept, and deliveries resume on their own when you are back on Growth or Scale. Events that happened in between are not sent afterwards.

## Duplicates and order

Build your handler around two facts:

**The same event can arrive more than once.** Ciarem guarantees each event is delivered *at least once*, not exactly once. A retry after your server processed the event but answered too slowly is the usual cause. Make handling safe to repeat:

* Store the `id` of each event you process and skip any `id` you have seen before. Keeping ids for a few days is enough, since retries stop after 3 days.
* Or make the action itself repeatable, for example "set the contact's stage to X" instead of "move the contact one stage forward".

**Events can arrive out of order.** A retried event arrives after newer ones, and two events seconds apart can swap places. Don't assume the last event you received is the latest state:

* Compare `created_at` on the envelope, or `updated_at` on the contact, with what you already stored, and ignore older data.
* For conversations, expect that `messages.inbound` can arrive before the `conversation.created` of the same conversation.
* When order matters, treat the event as a signal and read the current state from the [API](https://help.ciarem.ai/api-reference/overview).

## The delivery log

Every delivery of the last **30 days** is recorded, with the exact body that was sent.

**In the dashboard:** open **Settings → Webhooks** and click an endpoint. The **Deliveries** table shows each event, its status, your server's response code, and the number of attempts. Click a row to see the body that was sent, the event id, your server's response or the error, and the last and next attempts.

**With the API:**

```bash theme={null}
curl "https://api.ciarem.ai/v1/webhook-endpoints/whe_…/deliveries?status=failed&limit=50" \
  -H "Authorization: Bearer oak_YOUR_SECRET"
```

```json theme={null}
{
  "items": [
    {
      "id": "whd_…",
      "endpoint_id": "whe_…",
      "event_id": "evt_…",
      "event_type": "contact.updated",
      "status": "failed",
      "attempt_count": 3,
      "next_attempt_at": "2026-10-09T15:40:05.123456Z",
      "last_attempt_at": "2026-10-09T15:10:05.123456Z",
      "response_status": 500,
      "error": "endpoint answered 500",
      "created_at": "2026-10-09T15:04:05.123456Z",
      "payload": { "id": "evt_…", "type": "contact.updated", "…": "…" }
    }
  ],
  "next_cursor": null
}
```

| Parameter | |
| - | - |
| `status` | Optional. One of `pending`, `succeeded`, `failed`, `exhausted`. |
| `limit` | Optional. 1 to 100, default 20. |
| `cursor` | Optional. The `next_cursor` of the previous page. Results are newest first. |

`response_status` is `null` when your server never answered (a timeout or a connection error); `error` then says what went wrong.

Two things are **not** in the log: test events, and deliveries about a contact that has since been permanently erased, which are removed along with the contact's data.

## Sending a delivery again

You can resend any delivery in the log, whatever its status, for example after fixing a bug in your handler.

* **In the dashboard:** click the delivery to open it, then click **Send again**.
* **With the API:** `POST /v1/webhook-deliveries/{delivery_id}/redeliver`.

The delivery goes back to **Pending** and is sent **within a minute**, with the same body and the same event `id`. Because the `id` is unchanged, a handler that deduplicates will skip it: clear that `id` on your side first if you want it processed again.

Sending again is refused with `409 endpoint_disabled` while the endpoint is off. Enable it first.

## Catching up after downtime

Retries cover outages of up to 3 days. For anything longer, or for the time an endpoint was disabled, reconcile from the API:

* **Contacts:** list them with `GET /v1/contacts` and compare `updated_at` with what you hold.
* **Conversations:** list them with `GET /v1/conversations` and read the messages of the ones that changed.

A periodic reconciliation like this is good practice even when everything works. It protects you from bugs on your side that webhooks alone cannot reveal.

## Managing endpoints with the API

Everything in the dashboard is also available to an API key. Full request and response details are in the [API reference](https://help.ciarem.ai/api-reference/overview), under **Webhooks**.

| Request | Does |
| - | - |
| `GET /v1/webhook-endpoints` | Lists your endpoints. |
| `POST /v1/webhook-endpoints` | Creates an endpoint and returns its signing secret. |
| `GET /v1/webhook-endpoints/{id}` | Reads one endpoint. |
| `PATCH /v1/webhook-endpoints/{id}` | Changes `url`, `description`, `events`, or `enabled`. Send only the fields to change. |
| `DELETE /v1/webhook-endpoints/{id}` | Deletes the endpoint and its delivery log. |
| `POST /v1/webhook-endpoints/{id}/test` | Sends a test event now and returns your server's answer. |
| `GET /v1/webhook-event-samples` | Returns the sample body of every event type. |
| `GET /v1/webhook-endpoints/{id}/deliveries` | Lists an endpoint's deliveries. |
| `POST /v1/webhook-deliveries/{id}/redeliver` | Sends a delivery again. |

Errors specific to webhooks, on top of the [general ones](https://help.ciarem.ai/api-reference/errors-limits-pagination.md):

| Status | `detail` | Meaning |
| - | - | - |
| 422 | `{"code": "invalid_url", "message": "…"}` | The URL is not HTTPS, has no host, does not resolve, or points to a private address. `message` says which. |
| 422 | `"endpoint_limit_reached"` | The organization already has 10 endpoints. Delete one first. |
| 422 | field-level messages | For example an unknown event type, or `*` mixed with other types. |
| 404 | `"not_found"` | No endpoint or delivery with that id in your organization. |
| 409 | `"endpoint_disabled"` | You tried to send a delivery again while its endpoint is off. |

These calls count toward your [rate limit](https://help.ciarem.ai/api-reference/errors-limits-pagination.md#rate-limits) like any other API request. Events Ciarem sends **to you** do not.

## Troubleshooting

| Problem | What to check |
| - | - |
| **No events arrive at all** | Is the endpoint enabled? Is it subscribed to the event you expect? Send a test event: its result shows exactly what your server answered. Check that your plan includes webhooks. |
| **The test works but real events don't** | The endpoint is probably not subscribed to that event type. Open it and review **Events to send**. |
| **"This URL can't be used"** | The URL must be `https://` and resolve to a public address. Internal hostnames and `localhost` are refused. Use a tunnel for local development. |
| **Deliveries fail with a `301` or `302`** | Your server redirects, often from a URL without a trailing slash to one with it, or from a bare domain to `www`. Register the final URL. |
| **Deliveries fail with `401` or `403`** | Something in front of your server (a firewall, bot protection, or a login wall) blocks the request. Allow `POST` requests to that path. You can recognize Ciarem by the `Ciarem-Webhooks/1.0` user agent, but rely on the [signature](https://help.ciarem.ai/webhooks/signatures.md), not the user agent, for security. |
| **Deliveries fail with a timeout** | Your handler takes more than 10 seconds. Answer `200` first and do the work in the background. |
| **Deliveries fail with a TLS or certificate error** | Your certificate is expired, self-signed, or issued for a different hostname. |
| **The signature doesn't match** | See [When verification fails](https://help.ciarem.ai/webhooks/signatures.md#when-verification-fails). |
| **The same event arrives twice** | Expected now and then. Deduplicate on the event `id`. If it happens for every event, check whether two endpoints point at the same URL. |
| **`contact.updated` arrives several times for one edit** | Each custom property that changes sends its own event, and stage and owner changes send theirs. See the [event catalog](https://help.ciarem.ai/webhooks/events.md). |
| **An endpoint shows "Disabled after repeated failures"** | It failed every delivery for 3 days. Fix the cause, then **Enable** it. |
| **A delivery is missing from the log** | The log keeps 30 days. Test events are never logged. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.