Skip to content

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.