Webhooks

EntitleHub POSTs a signed event to your backend whenever a subscriber's state changes — so you can update your own database, kick off emails, or sync analytics without polling.

Register an endpoint

In Dashboard → Webhooks, add your HTTPS endpoint URL. EntitleHub generates a signing secret (whsec_…) — store it in your backend env. The dashboard shows a delivery log with each attempt and response.

Event payload

json
{
  "id": "evt_…",                       // unique — use it for idempotency
  "type": "initial_purchase",           // see event types below
  "app_id": "proj_…",
  "app_user_id": "user_123",
  "store": "play",
  "product": "app_pro_monthly",
  "entitlement": "pro",
  "active_entitlements": ["pro"],       // the subscriber's full active set after this event
  "environment": "production",           // or "sandbox"
  "created_at": "2026-07-18T00:00:00Z"
}
Sandbox purchases & restores

TestFlight / sandbox purchases fire webhooks just like production ones, with the full entitlement data and "environment": "sandbox". The subscriber's active_entitlements (and is_sandbox on entitlements in API responses) include sandbox-backed access so your app can unlock during testing — a production purchase always outranks a sandbox one for the same entitlement. Filter on environment before counting revenue or triggering anything money-related.

Restore purchases and app relaunches re-post the same store transaction; EntitleHub treats those as idempotent — the entitlement is refreshed but no new initial_purchase event fires. One store transaction, one sale.

Event types

  • initial_purchase — first purchase of a product.
  • renewal — a subscription renewed.
  • cancellation — auto-renew turned off (still active until expiry).
  • expiration — access ended.
  • billing_issue — payment failed / entered grace.
  • refund — a purchase was refunded / revoked.
  • trial_started, trial_converted — trial lifecycle.
  • override — a direct grant (no store purchase), e.g. a promo or code.
Renewals & lifecycle

initial_purchase and override fire on purchase / grant. renewal, cancellation, expiration, billing_issue, and refund fire from Apple / Google server-to-server notifications once you wire those up — see Store credentials.

Verify the signature

Each request includes an EntitleHub-Signature header. Recompute the HMAC over {timestamp}.{raw body} with your signing secret and compare — Stripe-style.

typescript
import crypto from "crypto";

function verify(rawBody: string, header: string, secret: string): boolean {
  // header looks like:  t=1737158400,v1=9f86d081...
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const expected = crypto.createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  // constant-time compare
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}

// In your handler: read the RAW body (not parsed) before verifying.

Delivery & retries

  • Respond 2xx quickly (within a few seconds). Do slow work asynchronously.
  • Failed deliveries are retried with backoff; every attempt is logged in the dashboard.
  • Deliveries can duplicate — dedupe on the event id.