Developer Docs
Webhook API
Overview

Overview

Joy Subscription delivers outbound webhooks (Joy server → your endpoint) so your integration can react to subscription lifecycle, billing, order, and trial events in near real time.

Webhooks are configured per shop and delivered over HTTP POST with a JSON body. Every delivery is signed with HMAC-SHA256 (the X-Signature header), carries an idempotency key for safe deduplication, and is retried on retryable failures.

Configuration

Webhook delivery is configured per shop on the integration / external-app settings, available under the Developers tab in the Shopify admin of the app. Each external app is configured with three values:

FieldDescription
addressYour receiver URL. Joy sends every delivery to this endpoint. Must be HTTPS.
apiKeyIdentifies the integration. Sent on every delivery in the X-API-Key header.
apiSecretSigning secret used to compute the HMAC X-Signature. Keep it server-side only.

You also select which topics to subscribe to. Only events for subscribed topics are delivered.

Topics

Topic names match Shopify's native webhook topic strings (trial topics are custom).

TopicTrigger
subscription_contracts/createA subscription contract is created.
subscription_contracts/updateA subscription contract is updated.
subscription_contracts/activateA contract is activated.
subscription_contracts/pauseA contract is paused.
subscription_contracts/resumeA paused contract is resumed.
subscription_contracts/cancelA contract is cancelled.
subscription_contracts/expireA contract reaches its end / expires.
subscription_billing_attempts/successA billing attempt succeeds.
subscription_billing_attempts/failureA billing attempt fails.
orders/createA subscription-related order is created.
orders/updateA subscription-related order is updated.
orders/cancelledA subscription-related order is cancelled.
trial/endingA trial is approaching its end (emitted by the trial cron).
trial/endedA trial has ended (emitted by the trial cron).

Notes:

  • trial/ending and trial/ended are custom topics produced by Joy's trial cron — Shopify has no native equivalent.
  • 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.
  • The public topic name is sent in the X-Shopify-Topic header.

Delivery model

  • Transport: HTTP POST with a JSON body to your configured address.
  • Content type: application/json.
  • Acknowledgement: respond with a 2xx status as fast as possible, then do the real work 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, or 408. Other 4xx responses are treated as non-retryable and the delivery is dropped after logging.
  • Idempotency: every delivery carries an idempotency key (header X-Idempotency-Key and idempotencyKey in the body). Retries reuse the same key, so deduplicate on it.

Request headers

Each delivery includes the following headers:

HeaderDescription
Content-Typeapplication/json.
X-API-KeyYour configured apiKey.
X-SignatureBase64 HMAC-SHA256 of the JSON body, keyed with your apiSecret.
X-Shopify-TopicThe Shopify-native topic name (e.g. subscription_contracts/cancel).
X-Shopify-DomainThe shop domain (e.g. example.myshopify.com).
X-Idempotency-KeyUnique key for this event; identical across retries.
X-TimestampISO 8601 timestamp of when the payload was built.

Payload envelope

All topics share one consistent payload shape: the topic-specific event fields, augmented by Joy with two delivery fields — timestamp (ISO 8601, mirrors X-Timestamp) and idempotencyKey (mirrors X-Idempotency-Key, default form <topic>-<uniqueId>-<shopDomain>).

Example payload for 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"
}

The HMAC signature is computed over the entire JSON body, including timestamp and idempotencyKey.

HMAC signature

Verify every delivery before trusting it. The X-Signature header is computed as:

X-Signature = base64( HMAC_SHA256( apiSecret, JSON.stringify(payload) ) )

where payload is the full JSON request body. To verify, re-serialize the parsed JSON body, recompute the HMAC, and compare with a timing-safe comparison. Reject mismatches with 401.

const crypto = require('crypto');
 
function verifyJoySignature(rawBody, signatureHeader, apiSecret) {
  const expected = crypto
    .createHmac('sha256', apiSecret)
    .update(rawBody)
    .digest('base64');
 
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Note: the signature is over JSON.stringify(payload) as produced by the sender. If your platform re-serializes JSON differently (key order, whitespace, unicode escaping), recompute from the parsed object using a serialization that matches the sender.

Product
Install on ShopifyWebsitePricing
Resources
DocumentationGetting StartedFAQsIntegrations
Company
Avada GroupContact Support
© 2026 Avada Group. All rights reserved.