Webhooks

Subscribe an endpoint

One call creates the endpoint and returns its signing secret. Here are the URL rules, who is allowed to subscribe, and what the refusals mean.

One call creates an endpoint and hands back its signing secret. Keep the secret: it is the only thing that lets your receiver prove a delivery came from us.

Who can subscribe

A workspace admin, or an API key carrying the admin scope. A webhook streams a workspace's activity to a URL of someone's choosing, which is an admin decision rather than a per-member one. Listing is open to any member, and so is removing an endpoint.

What a URL has to be

Subscribe an endpoint

curl -X POST https://api.engramdynamics.org/v1/webhooks \
  -H "Authorization: Bearer <your key>" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://hooks.example.com/engram",
        "events": ["corpus.ready", "sync_run.completed"]
      }' 
{
  "id": "w_6c4a...",
  "url": "https://hooks.example.com/engram",
  "secret": "whsec_Qv7...",
  "events": ["corpus.ready", "sync_run.completed"],
  "active": true,
  "last_delivery_at": null,
  "last_status": null,
  "created_at": "2026-09-13T10:12:20Z"
}

An empty events list subscribes to everything. Event names are validated against the closed set on Overview and events, and an unknown one is a 422 that lists the valid names.

hook = requests.post(
    f"{BASE}/webhooks",
    headers=HEADERS,
    json={"url": "https://hooks.example.com/engram", "events": []},
    timeout=30,
).json()

save_secret(hook["secret"])   # the HMAC key your receiver will need

List and remove endpoints

curl https://api.engramdynamics.org/v1/webhooks \
  -H "Authorization: Bearer <your key>"

curl -X DELETE https://api.engramdynamics.org/v1/webhooks/w_6c4a... \
  -H "Authorization: Bearer <your key>" 

The listing shows every endpoint the workspace has, with last_status and last_delivery_at, which is the fastest way to see that a receiver has started returning 500s. secret is included only for a workspace admin, or an API key with the admin scope; every other caller sees null. Anything holding the key can forge a delivery your receiver would accept, so the listing must not be the one place that hands it to everyone.

Delete is a real delete, not a flag, and it stops delivery immediately. The audit log keeps the record that the endpoint existed and when it went.

Replace a signing secret

Rotating gives an endpoint a new signing secret and keeps everything else about it, so the id your dashboards and scripts already hold does not change. Tenant admin, like subscribing.

curl -X POST https://api.engramdynamics.org/v1/webhooks/w_6c4a.../rotate -H "Authorization: Bearer <your admin key>"
{
  "id": "w_6c4a...",
  "url": "https://hooks.example.com/engram",
  "secret": "whsec_NEW...",
  "events": ["corpus.ready", "sync_run.completed"],
  "active": true,
  "last_delivery_at": "2026-09-13T10:11:43Z",
  "last_status": "200",
  "created_at": "2026-09-13T10:12:20Z"
}

The old secret stops verifying on that call. There is no overlap window, because the reason to rotate is that the old key is not trusted any more and a grace period would keep it alive for exactly as long as the grace period. Deliveries in between will fail your signature check, and if your receiver answers those with a 401 or a 403 we do not retry them — a 4xx tells us the request itself is wrong, so those events are gone. Two ways to rotate without losing any: put the new secret in your receiver first and rotate second, or have the receiver answer a signature it cannot verify with a 500 for the few minutes it takes you to deploy, which keeps the events on the retry ladder until it can.

Rotating is not plan-gated. A workspace on any tier can replace a leaked key, because an upgrade prompt in the middle of an incident is an ugly thing to do to somebody.

Pause and resume

Pausing stops the calls without losing the endpoint, which is what you want for a receiver going into maintenance or one that has started failing while you work out why.

curl -X PATCH https://api.engramdynamics.org/v1/webhooks/w_6c4a... -H "Authorization: Bearer <your admin key>" -H "Content-Type: application/json" -d '{"active": false}'

A paused endpoint gets nothing: no new deliveries, and no retries that were already scheduled when you paused it. Nothing is saved up while it is off, so resuming with {"active": true} starts from the next event rather than replaying the gap. Coming back to a backlog of stale "your run finished" calls would be worse than coming back quiet.

To catch up on what you missed, read the state rather than the events: GET /v1/corpora/{id}/sync-runs is the full ingestion history and GET /v1/corpora/{id} carries the readiness counts.

Both routes are also on the app: Settings → Webhooks has a pause toggle and a rotate button on every endpoint.

Limits and refusals

You will seeWhen
403 plan_featureThe workspace plan does not include webhooks. See Plans and allowances.
403, tenant admin requiredThe caller is a member, or a key without the admin scope.
409The workspace already has 10 endpoints, which is the ceiling (MAX_WEBHOOKS_PER_TENANT). Remove one you no longer use.
422An unknown event name, a non-HTTPS URL, a host that is not publicly routable, or a host that does not resolve at all.

Test before you rely on it

The cheapest end-to-end test is a real one. Subscribe your endpoint to sync_run.completed, upsert one small document into a throwaway document base, and watch it arrive:

curl -X POST https://api.engramdynamics.org/v1/corpora/c_7a1f.../documents/upsert \
  -H "Authorization: Bearer <your key>" \
  -H "Content-Type: application/json" \
  -d '{"path": "webhook-test.md", "text": "hello", "onboard": false}' 

Then check last_status on the endpoint. A 200 there means your receiver answered; anything else is the status it returned, or error for a connection failure.

Next

Verify a signature before your receiver acts on anything.