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