Webhook Events
Joy Subscription delivers outbound webhooks (Joy server → your endpoint) for subscription lifecycle, billing, order, and trial events. This page is the reference for the native event topics, their triggers, and the shared payload envelope.
Topic names match Shopify's native webhook topic strings — except the two trial topics, which are custom to Joy. The public topic name is sent in the X-Shopify-Topic header on every delivery.
Native topics
Subscription contracts
| Topic | Trigger |
|---|---|
subscription_contracts/create | A subscription contract is created. |
subscription_contracts/update | A subscription contract is updated. |
subscription_contracts/activate | A contract is activated. |
subscription_contracts/pause | A contract is paused. |
subscription_contracts/resume | A paused contract is resumed. |
subscription_contracts/cancel | A contract is cancelled. |
subscription_contracts/expire | A contract reaches its end / expires. |
Billing attempts
| Topic | Trigger |
|---|---|
subscription_billing_attempts/success | A billing attempt succeeds. |
subscription_billing_attempts/failure | A billing attempt fails. |
Orders
| Topic | Trigger |
|---|---|
orders/create | A subscription-related order is created. |
orders/update | A subscription-related order is updated. |
orders/cancelled | A subscription-related order is cancelled. |
orders/* topics only fire for subscription-related orders, identified by a Joy subscription note like Subscription #123 cycle 4, or by a selling-plan line item. Non-subscription orders are not forwarded.
Trial (custom)
| Topic | Trigger |
|---|---|
trial/ending | A trial is approaching its end (emitted by the trial cron). |
trial/ended | A trial has ended (emitted by the trial cron). |
trial/ending and trial/ended are custom topics produced by Joy's trial cron. Shopify has no native equivalent.
Payload envelope
All topics share one consistent payload shape. The body is the topic-specific event payload, augmented by Joy with two delivery fields:
| Field | Type | Description |
|---|---|---|
timestamp | string (ISO 8601) | When the payload was built. Mirrors the X-Timestamp header. |
idempotencyKey | string | Mirrors the X-Idempotency-Key header. Default form: <topic>-<uniqueId>-<shopDomain>. |
| (event fields) | object | Topic-specific fields. See the examples below. |
The HMAC signature in X-Signature is computed over the entire JSON body, including timestamp and idempotencyKey. Verify against the exact received bytes (or a re-serialized copy of the parsed JSON) before trusting any payload.
Examples
subscription_contracts/cancel
{
"subscriptionContractId": 123456789,
"status": "CANCELLED",
"reasonCode": "too_expensive",
"timestamp": "2026-06-24T10:00:00.000Z",
"idempotencyKey": "subscription_contracts/cancel-abc123-example.myshopify.com"
}subscription_billing_attempts/failure
{
"subscriptionContractId": 123456789,
"billingAttemptId": "…",
"errorCode": "…",
"timestamp": "2026-06-24T10:05:00.000Z",
"idempotencyKey": "subscription_billing_attempts/failure-def456-example.myshopify.com"
}orders/create
The orders/* payloads are projected to the documented order shape:
{
"orderId": 5544332211,
"subscriptionContractId": 123456789,
"cycleIndex": 4,
"totalPrice": "42.00",
"currency": "USD",
"timestamp": "2026-06-24T10:10:00.000Z",
"idempotencyKey": "orders/create-ghi789-example.myshopify.com"
}For orders/* events, both subscriptionContractId and cycleIndex may be null when the order is subscription-related via a selling-plan line item but carries no Joy subscription note to parse. Do not assume these fields are always present.
Request headers
Every delivery carries the headers below. Filter on X-Shopify-Topic and ignore topics you don't handle.
| Header | Description |
|---|---|
Content-Type | application/json. |
X-API-Key | Your configured apiKey. |
X-Signature | Base64 HMAC-SHA256 of the JSON body, keyed with your apiSecret. |
X-Shopify-Topic | The Shopify-native topic name (e.g. subscription_contracts/cancel). |
X-Shopify-Domain | The shop domain (e.g. example.myshopify.com). |
X-Idempotency-Key | Unique key for this event; identical across retries. |
X-Timestamp | ISO 8601 timestamp of when the payload was built. |
Delivery and retries
- Transport: HTTP
POSTwith a JSON body to your configuredaddress. - Acknowledgement: respond with a
2xxstatus as fast as possible, then process asynchronously. - Retries: on a retryable failure Joy makes up to 2 retries (3 attempts total). The base retry delay is
3000 ms, doubling each attempt (retryDelay * 2^(attempt-1)→ roughly 3s then 6s). - Retryable conditions: HTTP status
>= 500,429, or408. Other4xxresponses are treated as non-retryable and the delivery is dropped after logging. - Idempotency: retries reuse the same
idempotencyKey. Deduplicate on it so each event is processed at most once.