Developer Docs
Webhook API
Troubleshooting

Troubleshooting

This page covers the most common problems when receiving Joy Subscription outbound webhooks (Joy server → your endpoint): deliveries that never arrive, signature (HMAC) mismatches, and retry/timeout behavior. Each section lists what to check and how to debug it.

Webhook not arriving

If your endpoint is not receiving any deliveries, work through the checklist below in order.

1. Signing secret / credentials not configured

Webhook delivery is configured per shop on the integration / external-app configuration. Each external app is configured with three values:

FieldPurpose
addressYour receiver URL (where Joy POSTs the payload).
apiKeySent back to you in the X-API-Key header to identify the integration.
apiSecretUsed to sign each payload (HMAC-SHA256). See HMAC mismatch.

If the signing secret (apiSecret) has not been generated, or the credentials are incomplete, deliveries for that integration are skipped. Confirm all three values are set before debugging anything downstream.

2. Endpoint is not HTTPS or does not return 2xx

Joy expects your receiver to:

  • Be reachable over HTTPS (TLS). Plain HTTP endpoints are not supported.
  • Respond with a 2xx status to acknowledge receipt.

Any non-2xx response is treated as a failed delivery. Depending on the status code, the delivery is either retried or dropped — see Retries and timeouts.

A common mistake is doing heavy work before responding. Acknowledge with 2xx first, then process asynchronously, so the connection does not time out.

3. Topic not selected / not subscription-related

Confirm the event you expect actually maps to a forwarded topic:

  • The topic must be one of the supported topics (subscription_contracts/*, subscription_billing_attempts/*, orders/*, trial/ending, trial/ended).
  • orders/* topics only fire for subscription-related orders. An order is considered subscription-related when it carries a Joy subscription note like Subscription #123 cycle 4, or has a selling-plan line item. Non-subscription orders are never forwarded.
  • trial/ending and trial/ended are custom topics emitted by Joy's trial cron — they do not have a Shopify-native equivalent and only fire when the cron runs.

Inspect the X-Shopify-Topic header on any deliveries you do receive to confirm which topics are reaching you, and filter on it in your handler.

HMAC signature mismatch

If deliveries arrive but your signature check fails, the cause is almost always a serialization mismatch when recomputing the HMAC.

The X-Signature header is computed as:

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

where payload is the entire JSON request body, including the Joy-added timestamp and idempotencyKey fields.

Re-serialize the JSON to match the sender

Joy signs JSON.stringify(payload) exactly as produced by the sender. If your platform parses the body and then re-serializes it with different key order, whitespace, or unicode escaping, the recomputed digest will not match.

To avoid mismatches:

  • When possible, verify against the raw body bytes as the sender produced them, before any framework re-parses them.
  • If you only have the parsed object, re-serialize using a serialization that matches the sender:
    • Node.jsJSON.stringify(req.body).
    • Pythonjson.dumps(body_obj, separators=(",", ":")).
    • PHPjson_encode($bodyObj, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).
  • Always use a timing-safe comparison (crypto.timingSafeEqual, hmac.compare_digest, hash_equals).

Node.js verification example

const crypto = require('crypto');
 
function verifyJoySignature(rawBody, signatureHeader, apiSecret) {
  // rawBody: the exact JSON string received (or JSON.stringify of the parsed body).
  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);
}
 
// Express example
app.post('/webhooks/joy', express.json(), (req, res) => {
  const body = JSON.stringify(req.body); // Joy signs JSON.stringify(payload)
  if (!verifyJoySignature(body, req.get('X-Signature'), process.env.JOY_API_SECRET)) {
    return res.status(401).end();
  }
  // dedupe on req.get('X-Idempotency-Key'), then ack fast and process async
  res.status(200).end();
});

Python verification example

import base64, hashlib, hmac, json
 
def verify_joy_signature(body_obj, signature_header, api_secret):
    payload = json.dumps(body_obj, separators=(",", ":"))  # match the sender's serialization
    digest = hmac.new(api_secret.encode(), payload.encode(), hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()
    return hmac.compare_digest(expected, signature_header or "")

PHP verification example

<?php
function verify_joy_signature($bodyObj, $signatureHeader, $apiSecret) {
    $payload   = json_encode($bodyObj, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $expected  = base64_encode(hash_hmac('sha256', $payload, $apiSecret, true));
    return hash_equals($expected, $signatureHeader ?? '');
}

If verification still fails after matching serialization, confirm you are using the correct apiSecret for the shop sending the delivery (check X-API-Key and X-Shopify-Domain to identify the integration).

Retries and timeouts

Joy retries deliveries on retryable failures only:

ConditionBehavior
HTTP status >= 500, 429, or 408Retryable — Joy retries.
Other 4xx responsesNon-retryable — delivery is dropped after logging.

Retry policy:

  • Up to 2 retries after the initial attempt (3 attempts total).
  • Exponential backoff with a base delay of 3000 ms, doubling each attempt (retryDelay * 2^(attempt-1)), i.e. approximately 3s and then 6s.

Avoiding timeouts

Slow handlers risk timing out, which produces unnecessary retries. Acknowledge with 2xx as fast as possible and offload the real work to a queue or worker.

Controlling retries with your status code

Return the status code that matches what you want Joy to do:

  • Return 5xx, 429, or 408 to ask Joy to retry (e.g. your downstream is temporarily unavailable).
  • Return a non-retryable 4xx (such as 401 on a signature mismatch) only for permanently bad requests you never want retried.

Deduplicate retries with the idempotency key

Every delivery carries an idempotency key in both the X-Idempotency-Key header and the idempotencyKey body field. Retries reuse the same key (default form: <topic>-<uniqueId>-<shopDomain>). Deduplicate on it so a retried delivery is processed at most once, and keep your handler idempotent end-to-end since network conditions can still cause repeats.

Debugging checklist

Use the request headers on any received delivery to narrow down issues:

HeaderUse during debugging
X-Shopify-TopicConfirm which topic was delivered (e.g. subscription_contracts/cancel).
X-Shopify-DomainIdentify which shop sent the delivery.
X-API-KeyMatch the delivery to the correct integration / apiSecret.
X-SignatureThe value to verify against your recomputed HMAC.
X-Idempotency-KeyDeduplicate retries; correlate retried attempts.
X-TimestampISO 8601 time the payload was built (mirrors the body timestamp).

Suggested debugging steps:

  1. Receiving anything? Log every inbound request (before verification) and check whether deliveries reach your endpoint at all. If not, revisit Webhook not arriving.
  2. Right topic? Inspect X-Shopify-Topic and the payload fields against the topic you expect. Remember orders/* only fires for subscription-related orders.
  3. Signature passing? Log your recomputed signature alongside X-Signature, and log the exact bytes you signed. A difference points to a serialization mismatch — see HMAC signature mismatch.
  4. Returning 2xx fast? Measure handler latency. If you see repeated deliveries with the same X-Idempotency-Key, you are likely timing out or returning a retryable status; acknowledge first, then process async.
  5. Tolerating nulls? For orders/* payloads, subscriptionContractId and cycleIndex may be null. Do not assume their presence.
Product
Install on ShopifyWebsitePricing
Resources
DocumentationGetting StartedFAQsIntegrations
Company
Avada GroupContact Support
© 2026 Avada Group. All rights reserved.