MemorySyncMemorySync
How It Works

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

TableLifetimeKey columns
webhooksPermanent — registered once, lives until deleted.id, tenant_id, url, event_types, active, auth_header_name, auth_header_value.
webhook_deliveriesOne 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_lettersForever (operator can re-drive).Original payload, last error, last status code.

How an event becomes a delivery row

  1. 1Domain code emits an event (e.g. memory.created after the database commit).
  2. 2Dispatcher queries webhooks where the tenant matches and event_types contains memory.created.
  3. 3One webhook_deliveries row is created per matching webhook, status pending, payload pre-rendered (so the worker does not have to re-query).

How the worker drains the queue

  1. 1Worker polls every 5 seconds for up to 50 pending rows.
  2. 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).
  3. 3For each row: send the HTTP POST with the payload as the body and the configured auth header attached.
  4. 4On 2xx: row marked delivered, success timestamp recorded.
  5. 5On 4xx permanent errors (410, 403 on a deactivated webhook): row promoted to dead-letter immediately — retrying will not help.
  6. 6On 4xx retryable, 5xx, or timeout: retry_count++, scheduled_retry_at set per backoff schedule.
  7. 7Once retry_count reaches the per-webhook ceiling, row is promoted to dead-letter.

The payload shape

JSON
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.