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
{
"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"
}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.
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.
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
2xxquickly (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.
