Webhooks
Webhooks send content events to your HTTPS endpoint. When a record is created, updated, or deleted — the CMS makes a signed POST request to the configured URL.
How to set up
- Open Webhooks in the sidebar.
- Click Add webhook.
- Fill in:
- Name — a label for the list.
- Endpoint URL — the HTTPS address where the CMS will POST.
- Events —
entity.created,entity.updated,entity.deleted. Pick one or more. - Content types — scope filter. “All content types” = no filter.
- Active — enable/disable delivery.
- Save. The server generates a signing secret and shows it once — copy it. It can only be recovered by rotating the key.
Events
| Event | When it fires |
|---|---|
entity.created |
A new record of any type was created. |
entity.updated |
An existing record was changed. |
entity.deleted |
A record was deleted. |
The content-type scope filter narrows delivery: if you select only blog-post,
the webhook fires only for post changes, not every record in the project.
What the receiver gets
The CMS sends a POST with a JSON body:
{
"id": "whev_a1b2c3",
"tenant_id": "cf6efe09-...",
"type": "entity.created",
"entity_slug": "blog-post",
"entity_id": "550e8400-...",
"data": { "title": "Hello", "slug": "hello" },
"user": { "id": "u-1", "username": "alice", "role": "editor" },
"timestamp": "2026-08-11T12:00:00Z",
"attempt": 1
}
Headers:
| Header | Contents |
|---|---|
Content-Type |
application/json |
X-Dynamica-Signature |
HMAC-SHA256 signature (see below) |
X-Dynamica-Delivery |
Delivery ID — for idempotency |
X-Dynamica-Event |
Event type (entity.created) |
The prev_data field appears only on entity.updated — it carries the
state before the change. It is absent from other events.
Verifying the signature
Every request is HMAC-SHA256 signed with the secret you received when
creating the webhook. The signature is sent in the X-Dynamica-Signature
header as t=<unix seconds>,v1=<hex HMAC>. The HMAC is computed over the
string "<timestamp>.<raw request body>" — use the original bytes, do not
re-serialize the JSON.
Receiver-side verification:
- Parse
t(unix seconds) andv1(hex HMAC). - Reject if
|now - t| > 5 minutes. - Compute HMAC over the raw request body (do NOT re-serialize JSON!).
- Compare to
v1in constant time.
Example (Node.js):
const crypto = require('crypto');
function verify(secret, rawBody, header) {
const [tPart, v1Part] = header.split(',');
const ts = tPart.split('=')[1];
const v1 = v1Part.split('=')[1];
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(v1)
);
}
Delivery and retries
| Parameter | Value |
|---|---|
| Timeout | 10 seconds |
| Attempts | 8 (first + 7 retries) |
| Period | ~24 hours |
| Concurrency | 10 global, 3 per webhook |
Backoff schedule: 1 min → 5 min → 30 min → 2 h → 6 h → 12 h → 24 h.
HTTP response code behavior:
| Code | Action |
|---|---|
| 2xx | Success. Delivery complete. |
| 410 | Webhook is deactivated. No retries. |
| 4xx (other) | Client error. No retries. |
| 5xx | Retry. Each failure increments the counter. |
| Timeout / network | Retry. |
After 8 failures the delivery lands in dead letter — terminal status.
Delivery log
In the admin, below the webhook list — a log of the last 50 deliveries. For each: status, event, HTTP response code, attempt, the sent payload, and the receiver’s response body.
Statuses:
- Success — delivered (2xx).
- Retry — waiting for the next attempt.
- Pending — queued.
- Dead letter — all attempts exhausted.
- Circuit open — breaker suspended delivery (5 minutes).
- Partial failure — delivered, but command processing incomplete.
- Client error — 4xx, delivery halted.
Actions in the log:
- Test — send a test delivery.
- Replay — re-deliver a specific delivery.
- Rotate secret — generate a new signing secret.
Circuit breaker
If the receiver consistently responds 5xx or doesn’t answer, the circuit breaker opens: delivery is suspended for 5 minutes. Triggers: 20 consecutive failures or ≥50% failures out of the last 50 requests.
Limits
The number of webhooks depends on the plan. Checked on creation — if the limit is exceeded, the webhook is not created.
What’s next
- Plugins — reverse-proxy plugins with UI.
- GraphQL API — queries and mutations.
- Filtering — query filters.