# What is sent and how it is retried

The request your endpoint receives, the states of a delivery, the retry schedule, and disabling, for reference when you build and operate a receiver.

> **Type:** reference
> **For:** Anyone who implements a receiver or investigates a missing delivery.
> **Prerequisite:** For the delivery log in the portal, the Owner or Manager role. For redelivering, the Owner role.

## The request to your endpoint

Every delivery is a POST to the address of the endpoint. The body is the payload of the event as JSON, serialized compactly (without spaces and line breaks). Only the payload travels. The time of the event, its identifier, and the trigger are not in the body.

| Component | Value |
|---|---|
| Method | `POST` |
| `Content-Type` | `application/json` |
| `X-Signature` | HMAC-SHA256 over the raw body, with the signing secret of the endpoint, base64url encoded |
| `X-Event-Type` | The event type, for example `composition.scene.published` |
| `X-Delivery-Id` | The ID of the delivery (`whd_` and 26 characters). A retry carries the same one |
| Body | The payload of the event as JSON, the fields from the event tables |
| Timeout | 10 seconds for connection and response |

This is what a delivery of the event `composition.scene.published` looks like (body wrapped for readability, in reality it arrives on one line):

```http
POST /hooks/signage HTTP/1.1
Host: warenwirtschaft.example.com
Content-Type: application/json
X-Signature: 3q2-7wE5c1nXm0Zk8YbT4vLpQ9sRd6HfJgUaWi2xO7A
X-Event-Type: composition.scene.published
X-Delivery-Id: whd_01KXZACW98SENDNG0000000001

{
  "sceneId": "scn_01KJPX8NH0SZENE00000000001",
  "tenantId": "tnt_01KERPXZM0ADERAPTHEKE00001",
  "sceneVersionId": "scv_01KV5K0JE0VERSN00000000003",
  "versionNumber": 4,
  "contentHash": "b2f80a41d76c395ee0138b5a4c9d27f6103e8ab7592c4df081a63be95d7c024f",
  "appVersionPins": {},
  "publishedById": "usr_01KFB7QPW0TEAM000000000002",
  "at": "2026-07-20T08:31:05.000Z"
}
```

## The expected response

| Your response | Effect |
|---|---|
| Status 2xx within 10 seconds | The delivery counts as delivered. The failure counter of the endpoint resets to zero |
| 5xx, 408, or 429 | Temporary failure: retry according to the schedule below |
| No response within 10 seconds, connection error | Like a temporary failure: retry |
| Any other 4xx | Permanent failure: no retry, the delivery has failed for good |

On a failure, the response text is stored truncated (at most 4 kB) in the delivery log. On a successful delivery, only the status code is recorded. A short, descriptive text helps when you check later, but it is not required.

## Delivery states

| State in the API | In the portal | Meaning |
|---|---|---|
| `pending` | **Pending** | Sending is in progress or imminent |
| `delivered` | **Delivered** | Your server responded with 2xx. `deliveredAt` is set |
| `failed` | **Failed** | Temporary failure. `nextRetryAt` gives the next attempt |
| `dead` | `dead` | Failed for good: fifth failure or a 4xx response. `deadAt` is set |

The path is `pending` → `delivered` or `failed`. From `failed`, every further attempt leads back through `pending` to `delivered`, `failed`, or `dead`. For the state `dead`, the portal has no label of its own, and the delivery log shows the value itself. The label **Retrying** exists in the portal but is not assigned to any state.

## Retry schedule

The platform retries only on 5xx, 408, 429, timeouts, and network errors. There are at most five attempts per delivery. The wait after the nth failure is 1 minute · 2ⁿ, with ±20% jitter. A job that runs every five minutes picks up due retries, up to 100 per run, so the actual wait is up to five minutes longer than the target value.

| Attempt | Wait after failure | On another failure |
|---|---|---|
| 1 | 2 minutes ±20% | Attempt 2 |
| 2 | 4 minutes ±20% | Attempt 3 |
| 3 | 8 minutes ±20% | Attempt 4 |
| 4 | 16 minutes ±20% | Attempt 5 |
| 5 | — | Failed for good (`dead`) |

A 4xx response other than 408 and 429 ends the delivery immediately, regardless of the attempt counter.

## Redeliver by hand

In the delivery log of the endpoint, every delivery with the status **Failed** has the **Retry** button. For a delivery in the state `dead`, the portal shows no button. You trigger a redelivery through the API:

```bash
curl -X POST "$SZENERA_API/v1/webhooks/whk_01KPR20K40WEBHK00000000001/deliveries/whd_01KXZACW98SENDNG0000000001/retry" \
  -H "Authorization: Bearer $TOKEN"
```

The response (status 201) is the delivery with the outcome of the new attempt. The new attempt is signed with the currently valid secret of the endpoint and carries the same `X-Delivery-Id`. If the endpoint is inactive, nothing is sent, and the response carries the delivery unchanged. The API does not check the state: even a delivery that was already delivered can be sent again this way. Your receiver then sees the same ID a second time.

## Disabling an endpoint

Every failure raises the `consecutiveFailures` counter of the endpoint, and every success resets it to zero. If the counter is above 20, the endpoint becomes inactive (`active: false`), and the platform records `notifications.webhook.endpoint.disabled`. An hourly job makes up for what the counter missed at sending time. An inactive endpoint receives no new deliveries. In the portal it shows **Inactive** with the note “last failure …”. To turn it back on, use **Activate** or `PATCH /v1/webhooks/{id}` with `{"active": true}`. This does not take effect immediately: for each event type, the platform remembers for up to an hour whether an active endpoint has subscribed to it. After activating, or after adding events, it can therefore take up to an hour until the first delivery of a newly subscribed type arrives. Newly created endpoints take effect immediately.

## The delivery log through the API

```bash
curl "$SZENERA_API/v1/webhooks/whk_01KPR20K40WEBHK00000000001/deliveries?limit=50" \
  -H "Authorization: Bearer $TOKEN"
```

The response is a page with `items` and `nextCursor` (if there is more). The next call gets `cursor=<nextCursor>`. `limit` is between 1 and 200, and 50 if omitted.

```json
{
  "items": [
    {
      "id": "whd_01KXZACW98SENDNG0000000001",
      "tenantId": "tnt_01KERPXZM0ADERAPTHEKE00001",
      "endpointId": "whk_01KPR20K40WEBHK00000000001",
      "eventType": "composition.scene.published",
      "payloadSignature": "3q2-7wE5c1nXm0Zk8YbT4vLpQ9sRd6HfJgUaWi2xO7A",
      "status": "delivered",
      "attemptCount": 1,
      "responseCode": 200,
      "responseBody": null,
      "nextRetryAt": null,
      "dispatchedAt": "2026-07-20T08:31:05.000Z",
      "deliveredAt": "2026-07-20T08:31:06.000Z",
      "deadAt": null,
      "createdAt": "2026-07-20T08:31:05.000Z"
    }
  ],
  "nextCursor": "d2hkOjIwMjYtMDctMTk"
}
```

| Field | Meaning |
|---|---|
| `id` | ID of the delivery, the same as in `X-Delivery-Id` |
| `endpointId`, `tenantId` | Endpoint and organization |
| `eventType` | The event type |
| `payloadSignature` | The signature that was sent as `X-Signature` |
| `status` | `pending`, `delivered`, `failed`, or `dead` |
| `attemptCount` | Number of attempts so far |
| `responseCode`, `responseBody` | Response from your server. `responseCode` is `null` if there was none. `responseBody` is filled only for failed attempts (truncated to 4 kB) and after a successful delivery is `null` or still carries the text of the last failed attempt |
| `nextRetryAt` | Next scheduled attempt, only for `failed` |
| `dispatchedAt`, `deliveredAt`, `deadAt`, `createdAt` | Times of the last send, the delivery, the final failure, and creation |

The body that was sent is not in the list.

## Permissions

| Action | Permission | Roles |
|---|---|---|
| Read the delivery log | `webhook.delivery.list` | Owner, Manager |
| Redeliver | `webhook.delivery.retry` | Owner |

These permissions apply to signed-in accounts. An API key (`ak_…`) cannot carry them. The calls need the access token of a sign-in, see [Authentication](/api/authentication).

## Related

- [How is a webhook delivered?](/webhooks/about-delivery)
- [Verify the signature of a delivery](/webhooks/verify-signature)
- [When no delivery arrives](/webhooks/troubleshooting)
