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

# Event webhooks

> Push signed events to your own HTTPS endpoint when something changes, with retries, a dead letter state, a delivery log and one click replay.

Event webhooks tell your own systems when something changed on the control plane: an app was deployed, a user was added, a token was revoked. Each event is a signed JSON POST to an HTTPS endpoint you choose. Delivery is queued and runs in the background, so a slow or dead endpoint never slows an API request or a deploy.

For a continuous copy of the whole audit log to a SIEM, syslog server or bucket, use [Data export](/data-export). Event webhooks are for reacting to individual changes, and they keep a per-delivery log you can replay from.

<InlineToc default-open />

## What triggers an event

Every successful change made through the API becomes one event, named `<resource>.<verb>`, for example `apps.create`, `apps.delete`, `apps.deploy`, `users.update`. Failed requests and read requests produce no event. A subscription can list exact types, a family such as `apps.*`, or nothing to receive every event.

## Add a webhook

From **Settings, Event webhooks** in the dashboard, or:

```bash theme={"dark"}
levelrail-cli event-webhooks create --name siem --url https://hooks.example.com/levelrail --event 'apps.*,users.*'
```

The signing secret is printed once. It is stored encrypted and never returned by the API again. Lost it? Run `levelrail-cli event-webhooks rotate-secret <id>` to issue a new one.

The URL must be a public http(s) address. Internal addresses are refused unless `APP_EXPORT_ALLOW_PRIVATE_NETWORKS` is set. Creating webhooks needs a master key, because signing secrets are stored with envelope encryption.

## Payload

```json theme={"dark"}
{
  "id": "evt_9f2c1a...",
  "type": "apps.deploy",
  "created_at": "2026-10-14T09:30:00Z",
  "data": {
    "actor_type": "session",
    "actor_name": "admin",
    "method": "POST",
    "path": "/api/v1/apps/web/deploy",
    "status": 202,
    "audit_id": "aud_..."
  }
}
```

## Verify the signature

Every request carries these headers, with the brand short name as the prefix (shown here as `X-Brand`):

| Header | Value |
| - | - |
| `X-Brand-Event` | event type |
| `X-Brand-Event-Id` | event id, stable across retries and replays |
| `X-Brand-Signature` | `t=<unix>,v1=<hex>` |

The signature is an HMAC-SHA256, keyed with the signing secret, over `"<t>.<raw body>"`. Recompute it over the raw bytes, compare in constant time, and reject timestamps older than a few minutes to stop replayed captures.

```python theme={"dark"}
import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    mac = hmac.new(secret.encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) <= tolerance
    return fresh and hmac.compare_digest(mac, parts["v1"])
```

## Delivery, retries and the dead letter state

A delivery succeeds on any 2xx answer. Everything else is retried with exponential backoff, doubling from 30 seconds up to one hour, for up to 8 attempts. A 4xx answer other than 408 and 429 is treated as permanent and goes straight to `dead`, since retrying cannot fix it.

Delivery is at least once. If your endpoint accepts an event but the control plane fails to record that, the event is sent again with the same `X-Brand-Event-Id`, so deduplicate on it.

| State | Meaning |
| - | - |
| `pending` | waiting for the next attempt |
| `delivered` | the endpoint answered 2xx |
| `dead` | out of attempts or permanently rejected, replay it once the endpoint is fixed |

## Test, inspect and replay

* **Send test** (`levelrail-cli event-webhooks test <id>`) sends a signed `webhook.test` event immediately and shows the HTTP status or the error.
* **Delivery log** (`levelrail-cli event-webhooks deliveries <id>`) lists each event with its state, attempts, last HTTP status and last error.
* **Replay** (`levelrail-cli event-webhooks replay <delivery-id>`) puts a delivery back in the queue with a fresh attempt budget.

Pause a webhook with `disable`. Disabled webhooks do not queue new events.

## Tuning

| Variable | Default | Meaning |
| - | - | - |
| `APP_EVENT_WEBHOOK_MAX_ATTEMPTS` | `8` | attempts before a delivery is dead lettered |
| `APP_EVENT_WEBHOOK_BACKOFF_BASE` | `30s` | wait before the first retry |
| `APP_EVENT_WEBHOOK_BACKOFF_MAX` | `1h` | cap on the retry wait |
| `APP_EVENT_WEBHOOK_TIMEOUT` | `10s` | per request timeout |
| `APP_EVENT_WEBHOOK_RETENTION` | `720h` | how long finished deliveries are kept |
| `APP_EVENT_WEBHOOK_QUEUE_SIZE` | `1024` | in memory buffer, events beyond it are dropped, never waited on |

## Limits

Events are generated from audited API changes. Changes made by the control plane itself, such as an automatic rollback, are not events yet.


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