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, limit, or value. 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 / value 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 when this attempt was sent (new on each retry)
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 pause the endpoint. A 2xx resets the counter. Resume in the console, with POST /org/webhook/enable, or by saving the URL again.
Duplicate id Ignore.
Missed or delayed POST Serve cache until TTL, then GET or check.
No URL We enqueue nothing.
Endpoint paused (by you or automatically) Events queue and wait. They're delivered when you resume. Pause it yourself before maintenance: Pause and resume.
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.