Webhooks¶
Webhooks are optional. They matter if you cache entitlements: when something changes, e10s POSTs to your URL so you can drop that snapshot. You can also stay fresh with a TTL, or both, or just call check.
The POST is not the new entitlement — it does not carry allowed or limit. Refill with GET entitlements or POST check.
Configure the URL in the console. Event JSON: Webhooks. Ways to check: Check access.
If you cache¶
| Do | Don't |
|---|---|
Cache allowed / limit per subscriber (tag with plan key if you have it) |
Cache usage or remaining |
Refill with GET …/entitlements or POST check |
Treat the webhook body as the new check result |
Use ?all=true if you cache denies |
Assume the default list includes denies — it is entitled only |
| Drop on event, refill on next use | GET every subscriber in the handler (eval burst is 100/s) |
| Keep a TTL even if you also use webhooks | Rely on webhooks with no TTL — a missed POST is stale forever |
Missing from the default list means not entitled, unless you fetched ?all=true.
Events¶
Two types: entitlements.stale (one subscriber) and plan.grants.updated (one plan, no subscriber list). JSON and when we fire: Webhooks.
Verify¶
We POST JSON. The signature uses the raw body bytes, not a re-serialized object.
| Header | Meaning |
|---|---|
X-E10s-Webhook-Id |
Event id (ULID). Same on retries. Dedup key. |
X-E10s-Webhook-Timestamp |
Unix seconds |
X-E10s-Webhook-Signature |
Hex HMAC-SHA256 of secret over ${timestamp}.${rawBody} |
import { createHmac, timingSafeEqual } from "node:crypto"
function verifyWebhook(
secret: string,
timestamp: string,
rawBody: string,
signature: string,
): boolean {
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
const a = Buffer.from(expected, "hex")
const b = Buffer.from(signature, "hex")
if (a.length !== b.length) return false
return timingSafeEqual(a, b)
}
Return 2xx once you have accepted the event (dropped cache / recorded id). Do not wait on refill.
Failure¶
| Situation | Behavior |
|---|---|
| Your endpoint returns 2xx | Delivered. Retries stop. |
| Non-2xx or timeout (5s) | We retry. Same id. At-least-once. |
| 8 attempts exhausted | That event stops. Eight consecutive exhausted events disable the endpoint. A 2xx resets the counter. Enable in the console. |
Duplicate id |
Ignore. |
| Missed or delayed POST | Serve cache until TTL, then GET or check. |
| No URL, or endpoint disabled | We enqueue nothing. |
plan.grants.updated |
Do not GET N subscribers in the handler. |
Note
Check already ignores an expired override at expiry time, even if the expiry event has not arrived. The event is for caches that still hold the pre-expiry value.