Webhook Dispatch
Webhooks are how MemorySync tells your systems that something happened — a memory was created, a user was deleted, a sync finished. This page describes how a registered webhook turns into an outbound HTTP call, how delivery is retried, and what dead-letter looks like.
The three rows that describe a webhook
| Table | Lifetime | Key columns |
|---|---|---|
webhooks | Permanent — registered once, lives until deleted. | id, tenant_id, url, event_types, active, auth_header_name, auth_header_value. |
webhook_deliveries | One row per attempt-batch. Lives until retention sweep purges it. | id, webhook_id, event_id, payload_json, status, retry_count, last_attempt_at, scheduled_retry_at. |
webhook_dead_letters | Forever (operator can re-drive). | Original payload, last error, last status code. |
How an event becomes a delivery row
- 1Domain code emits an event (e.g.
memory.createdafter the database commit). - 2Dispatcher queries
webhookswhere the tenant matches andevent_typescontainsmemory.created. - 3One
webhook_deliveriesrow is created per matching webhook, statuspending, payload pre-rendered (so the worker does not have to re-query).
How the worker drains the queue
- 1Worker polls every 5 seconds for up to 50
pendingrows. - 2Per-target concurrency is bounded by a semaphore (default 10 in flight globally, with a per-endpoint cap so one slow customer cannot starve the rest).
- 3For each row: send the HTTP
POSTwith the payload as the body and the configured auth header attached. - 4On
2xx: row markeddelivered, success timestamp recorded. - 5On
4xxpermanent errors (410,403on a deactivated webhook): row promoted to dead-letter immediately — retrying will not help. - 6On
4xxretryable,5xx, or timeout:retry_count++,scheduled_retry_atset per backoff schedule. - 7Once
retry_countreaches the per-webhook ceiling, row is promoted to dead-letter.
The payload shape
POST https://your-system.example.com/webhooks/memorysync HTTP/1.1
X-MemorySync-Event: memory.created
X-MemorySync-Delivery: wd_8821
X-MemorySync-Signature: t=1714817532,v1=<hex>
Content-Type: application/json
{
"id": "evt_3f9c1ab2",
"type": "memory.created",
"occurred_at": "2026-05-04T10:14:32Z",
"tenant_id": "tnt_a1b2c3d4",
"data": {
"memory_id": 18421,
"project_id": "proj_a1b2c3d4e5f6a7b8",
"tier": "hot",
"tags": ["preference", "ui"]
}
}How to verify the signature on your side
The X-MemorySync-Signature header carries a timestamp and an HMAC of {timestamp}.{raw_body}. Reject deliveries where the timestamp is older than 5 minutes and recompute the HMAC with your webhook's signing secret to validate authenticity. Two-step verification stops replay even if the secret is briefly visible in a log.
Graceful shutdown — no half-finished deliveries
On API server shutdown, the webhook worker stops accepting new pending rows and waits up to 30 seconds for in-flight deliveries to finish. Anything still in flight after the grace window is left in retry_count++ state so the next worker that boots picks it up cleanly. There are no half-acknowledged deliveries — either the row is delivered or it is going to be retried.
Three things to check when deliveries stop arriving
- Is the webhook
active? Webhooks toggle to inactive automatically after enough consecutive dead-letters; they need re-enabling. - Is your endpoint returning quickly? Slow endpoints fill the per-endpoint semaphore and drop you behind every other tenant.
- Is the dead-letter table growing? Every dead-letter row carries the full payload and the last error — fix the root cause, then bulk re-drive.