Events & webhooks
An event describes something that changed, such as Alice canceling renewal. A webhook is the HTTP message that carries that event from the provider to your backend.
Use events to keep your backend or data service informed while the app is closed. Cancellation turns renewal off; it does not remove Alice’s remaining paid time. Read current entitlements when you need an authoritative access decision.
Retries keep one inbox entry
openiap-commerce-protocol-example
A local worker signs and retries events. The included receiver verifies the signature and saves each event once in a separate SQLite inbox. Use it to understand both ends of delivery.
Read code: deliver / createReceiverThe delivery worker sends signed events to registered backend HTTPS endpoints and records attempts. Your receiver owns duplicate handling; the example shows how to build that side.
Read code: deliverPendingEventsHandlerHow to check this behavior
Example. Checks cover signatures, retries, tampering, and reopening the inbox. Run npm test or npm run demo:consumer. Inspect reference
IAPKit. Worker tests check signed requests, transport failures, and destination validation with test I/O. Inspect reference
Store → conforming backend → consumer HTTPS endpointTo receive events in your product, start with the working receiver and setup guide. Run npm run demo:consumer in the example project to see accepted, repeated, and tampered deliveries before connecting a provider.
Request#
The emitter sends POST to a public HTTPS URL supplied directly by the consumer. The content type is application/json, and the body is one Commerce Protocol event document.
| Header | Value | Role |
|---|---|---|
openiap-signature | v1=<hex>, optionally repeated during rotation | Verified against the exact body bytes |
openiap-timestamp | Unix seconds at signing | Part of the signed input |
openiap-event-id | The eventId value | Convenience only; trust the signed body |
openiap-delivery-id | One delivery-attempt chain | Operational correlation only |
Headers help route and inspect a delivery, but the signed body is the authority. Read eventId from the parsed body rather than trusting the convenience header.
Event vocabulary#
Known event types named by Protocol 1.0 are:
subscription.started, subscription.renewed, subscription.recovered, subscription.entered_grace_period, subscription.entered_billing_retry, subscription.expired, subscription.canceled, subscription.uncanceled, subscription.revoked, subscription.refunded, subscription.product_changed, subscription.price_changed, subscription.deferred, subscription.paused, subscription.resumed, entitlement.granted, entitlement.revokedThe schema value space is open: a later MINOR version may add another type. Read the event schema for the normative fields and apply only event types your consumer understands.
Verify the signature before parsing#
signature = "v1=" + lowercase_hex(
HMAC_SHA256(secret, timestamp + "." + exactBodyBytes)
)- Read the raw request bytes. Re-serializing JSON changes the signed input.
- Accept only when
|now - timestamp| <= 300seconds; otherwise reject it. - Split
openiap-signatureon commas and accept any validv1=value. Rotation may supply two signatures. - Compare signatures in constant time.
- Parse and validate the verified body, then persist and deduplicate its
eventIdwithin the configured emitter/project scope before acknowledging delivery. An ID from one emitter must not suppress another emitter's event.
Delivery is duplicate-capable and unordered#
An emitter retries transient failures with exponential backoff and eventually stops and dead-letters an unaccepted delivery. A consumer may receive zero, one, or several copies. Acknowledge after durable ingestion and before slow downstream work. Keep those downstream effects idempotent on the stable eventId.
| Consumer response | Emitter behavior |
|---|---|
2xx | Delivered |
408, 429, 5xx | Retry |
3xx | Permanent failure; do not follow |
Other 4xx | Permanent failure; do not retry |
Timeout or connection error | Retry |
Retries and independent queues can reorder events. Consumers use occurredAt to prevent an older snapshot from overwriting newer state, while still processing independent idempotent effects.
A subscription's active value is a snapshot at processedAt. Never grant access at or after its expiresAt; refresh current access when needed. A cancellation stops renewal and does not remove the remaining paid period. See ongoing access.
Destination safety#
Emitters accept public HTTPS destinations only. They reject embedded credentials and loopback, private, link-local, or unique-local addresses; validate every resolved address; and do not follow redirects. They connect only to a validated public address, by pinning it or verifying the connected peer before sending bytes.
Normative resources#
- Webhook contract — request, signature, response, delivery, and destination rules
- GraphQL contract — the human-readable wire structure and validation directives
- Signature vectors — reproducible valid and rejected cases