Skip to content

Webhooks

Outbound POST to one URL per organization (http or https). Events mark a cache stale; they are not a feed of resolved entitlements and do not include usage or catalog CRUD.

If you cache: Webhooks. Configure: Console.

Headers

We POST JSON. HMAC is over the raw body.

Header Rule
X-E10s-Webhook-Id Event id (ULID). Same on every retry. Dedup key.
X-E10s-Webhook-Timestamp Unix seconds when this attempt was sent (changes on each retry, so the signature does too)
X-E10s-Webhook-Signature Hex HMAC-SHA256(secret, ${timestamp}.${rawBody})

entitlements.stale

This subscriber’s entitled values may have changed.

{
  "id": "01H…",
  "type": "entitlements.stale",
  "occurred_at": "2026-03-01T09:00:00.000Z",
  "organization_id": "…",
  "subscriber_id": "cust_123",
  "cause": "override",
  "features": ["sso"]
}
Field Rule
id ULID. Same on every retry. Dedup key.
occurred_at Canonical UTC (YYYY-MM-DDTHH:mm:ss.sssZ)
organization_id Your vendor org
subscriber_id The subscriber whose snapshot to drop
cause subscription | override | expiry | subscriber
features Present when we know the keys. Omitted = drop the whole snapshot

No allowed, limit, value, or source.

plan.grants.updated

This plan’s grants changed. One POST. No subscriber id list.

{
  "id": "01H…",
  "type": "plan.grants.updated",
  "occurred_at": "2026-03-01T09:00:00.000Z",
  "organization_id": "…",
  "key": "pro"
}

Drop every cached snapshot, or those tagged with this plan key. Refill on next use.

Canceled subscribers already lost plan grants; they are not implied by this event. They still get override and expiry events.

When we fire

After the write commits, only if resolution could change. No event on 4xx. Check and GET never emit. Write 2xx does not wait for delivery.

Write Event
POST …/subscriptions entitlements.stale cause: subscription — omit features
POST …/subscriptions/change one entitlements.stale cause: subscription — not cancel+create
POST …/subscriptions/{id}/cancel entitlements.stale cause: subscription
PUT …/overrides/{feature} when kind/allowed/limit/value or current effectiveness changes entitlements.stale cause: override features: [feature]
PUT identical override (same kind/allowed/limit/value, a JSON value in any key order; expires_at only moves further into the future, or null↔future with the same value) none
PUT that sets expires_at in the past, or changes the live value entitlements.stale cause: override
DELETE …/overrides/{feature} entitlements.stale cause: override features: [feature]
Override expires_at is reached entitlements.stale cause: expiry features: [feature]
DELETE …/subscribers/{id} entitlements.stale cause: subscriber — omit features (refetch 404)
PATCH …/plans/{key} grants actually differ plan.grants.updated
PATCH plan name only, or grants identical none
POST plan / DELETE plan none — create has nobody on it; delete is 409 if any subscription exists
POST …/plans/{key}/archive or …/unarchive none — existing subscribers stay on the plan
Subscriber create / rename none
Feature create / rename / delete none — create entitles nobody; delete is 409 if referenced
GET entitlements / POST check none

Check ignores an expired override at expires_at even if the expiry event has not been delivered.

Delivery

  • One URL per organization. No URL → we enqueue nothing.
  • At-least-once. id is stable across retries. Your 2xx = done. Timeout 5s.
  • First attempt right away. Each retry waits after the previous failed attempt: 30s, 5m, 30m, 2h, 5h, 10h, 24h (8 attempts in all). Then stop.
  • Each attempt is signed with the secret current at send time. Rotating takes effect immediately, including for retries of earlier events; there is no overlap window.

Pause and resume

Deliveries are either on or paused (disabled: true in GET /org/webhook). There are two ways to get paused:

  • You pause it: the console's Pause deliveries button, or POST /org/webhook/disable. Use this for your own maintenance or an outage on your side.
  • Auto-disable: 8 events in a row each run out of retries. A 2xx resets that count.

While paused, events are still queued and nothing is sent, not even retries. To resume, use Resume deliveries in the console or POST /org/webhook/enable. Saving a new URL (PUT /org/webhook) also resumes, so set the new URL last. On resume, everything queued is due right away and goes out in the order it was scheduled. Attempts already made still count toward the 8. DELETE /org/webhook is different: it removes the endpoint, and nothing is queued after that.

Config

e10s.io → Settings → Webhooks, or call these routes with a member API key whose role has webhooks:read / webhooks:write (developer or admin to change it). Incoming POSTs authenticate with HMAC, not X-API-Key.

Plaintext secret (e10s-wh-1-…) is only in create and rotate responses. GET returns secret_prefix (first 12 characters), never the secret.

Method Path Label Result
GET /org/webhook webhooks:read 200 { url, secret_prefix, disabled, created_at, updated_at } · 404 if none
PUT /org/webhook webhooks:write { "url": "https://…" } create or replace URL. 200 either way. Create returns secret. URL change keeps the secret (no secret field) and resumes a paused endpoint.
POST /org/webhook/rotate webhooks:write New secret (shown once). 404 if none
POST /org/webhook/disable webhooks:write Pause deliveries; events keep queueing. 200 endpoint · 404 if none
POST /org/webhook/enable webhooks:write Resume deliveries (after a pause or auto-disable). 200 endpoint · 404 if none
DELETE /org/webhook webhooks:write 204. Later writes enqueue nothing.

url must be http or https, no credentials, max 2048.