Skip to content

Kaching Subscriptions Webhooks Documentation

Get notified the moment a subscription contract changes. Kaching delivers a signed POST request to your endpoint for each event you subscribe to, so you can sync subscriptions, trigger emails, update a CRM, or kick off fulfilment without polling the API.

Every delivery is signed with HMAC-SHA256 so you can verify it came from Kaching.

Configure webhooks in the Kaching Subscriptions admin under Settings → Integrations → Webhooks.

For each event you want to receive:

  1. Enter an HTTPS endpoint URL (e.g. https://example.com/webhooks/kaching).
  2. Toggle the event on.

A shop-level signing secret is generated and shown once when you first save — copy it and store it securely. You can reveal, copy, or regenerate it at any time. The same secret signs every event for the shop. Use Send test to deliver a sample payload to a saved endpoint.

Each event is identified by a topic. The topic is sent both in the request body and in the X-Kaching-Topic header.

TopicFires when
subscription_contract/createdA new subscription contract is created.
subscription_contract/pausedA contract is paused.
subscription_contract/resumedA paused contract is resumed.
subscription_contract/cancelledA contract is cancelled.
subscription_contract/upcoming_chargeThe pre-renewal reminder is sent, before a renewal.
billing_attempt/successA renewal payment succeeds.
billing_attempt/failureA renewal payment fails.
order/skippedAn upcoming subscription order is skipped.
order/unskippedA previously skipped order is restored.

Webhooks are delivered as an HTTP POST with Content-Type: application/json.

Headers

HeaderValue
X-Kaching-TopicThe event topic, e.g. subscription_contract/created.
X-Kaching-Webhook-TimestampUNIX timestamp (seconds) when the request was signed.
X-Kaching-Hmac-Sha256HMAC-SHA256 signature of the request, as 64-char lowercase hex.

Body

The body is a JSON envelope with the topic and an data object:

{
"topic": "subscription_contract/created",
"data": {
/* SubscriptionEventData — see Payload reference */
}
}

Verify every request before trusting it. The signature proves the request came from Kaching and was not tampered with.

The signature is computed over the string `${timestamp}.${rawBody}`, where:

  • timestamp is the value of the X-Kaching-Webhook-Timestamp header, and
  • rawBody is the exact raw request body — the bytes as received, before any JSON parsing or re-serialization.

To verify:

  1. Read the X-Kaching-Webhook-Timestamp and X-Kaching-Hmac-Sha256 headers.
  2. Compute HMAC_SHA256(signingSecret, "${timestamp}.${rawBody}") and hex-encode it.
  3. Compare it to X-Kaching-Hmac-Sha256 using a constant-time comparison.
  4. Reject the request if the timestamp is too old. Compare X-Kaching-Webhook-Timestamp to your current time and drop anything outside a tolerance window — ±5 minutes (300 seconds) is a reasonable default. This is the anti-replay guard: the timestamp is part of the signed string, so an attacker who captures a valid request can’t replay it indefinitely — the signature stays valid but the request “expires” once it falls outside the window. Use a wider window if your server clock may drift relative to Kaching’s.

Example (Node.js)

const crypto = require("crypto");
// `rawBody` must be the raw request body string, NOT a re-serialized object.
function verifyKachingWebhook(rawBody, headers, signingSecret) {
const timestamp = headers["x-kaching-webhook-timestamp"];
const signature = headers["x-kaching-hmac-sha256"];
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature),
);
// Anti-replay: reject requests older than 5 minutes (300 seconds). Without
// this, a captured signed request stays valid forever and could be replayed.
// `Math.abs` also tolerates minor clock skew in either direction.
const ageSeconds = Math.abs(
Math.floor(Date.now() / 1000) - Number(timestamp),
);
const TOLERANCE_SECONDS = 300; // 5 minutes — tune to your clock skew
return valid && ageSeconds < TOLERANCE_SECONDS;
}
  • Acknowledge with any 2xx status (200299). Respond within 10 seconds — the request times out after that. Defer heavy processing to a background job and return quickly.
  • Retries: network errors, timeouts, 429, and 5xx responses are retried. Other 4xx responses are treated as terminal and are not retried — fix a misconfigured endpoint and re-save it.
  • No ordering or idempotency guarantees: deliveries are asynchronous and retries can deliver the same event more than once. Deduplicate on your side — for example, key on contractId + topic + the timestamp header.

The data object is the same vendor-neutral SubscriptionEventData shape for every topic. Below is a full example — exactly what the Send test button delivers for subscription_contract/created.

{
"topic": "subscription_contract/created",
"data": {
"contractId": "44017680677",
"subscriptionCreatedAt": "2026-03-26T20:17:17Z",
"currencyCode": "USD",
"customer": {
"id": "1000000000000",
"email": "example@gmail.com",
"firstName": "John",
"lastName": "Doe",
"displayName": "John Doe",
"lifetimeDuration": "about 2 months",
"locale": "en-US",
"numberOfOrders": 0,
"tags": [
"kaching-has-active-subscription",
"kaching-has-cancelled-subscription",
"kaching-has-paused-subscription",
"kaching-has-payment-failure",
"kaching-subscriber"
],
"isEmailVerified": true
},
"linesCount": 2,
"lines": [
{
"title": "Hat",
"variantTitle": null,
"productId": "10000000000001",
"variantId": "10000000000002",
"quantity": 1,
"variantImageUrl": "https://subscriptions-assets.kachingappz.app/products/hat.jpeg",
"variantImageAltText": null,
"lineDiscountedPrice": 10,
"lineDiscountedPriceFormatted": "$10.00"
},
{
"title": "Blue Shirt",
"variantTitle": null,
"productId": "10200405213477",
"variantId": "51451354415397",
"quantity": 1,
"variantImageUrl": "https://subscriptions-assets.kachingappz.app/products/shirt.jpeg",
"variantImageAltText": null,
"lineDiscountedPrice": 10,
"lineDiscountedPriceFormatted": "$10.00"
}
],
"subtotalPrice": { "amount": 20, "currencyCode": "USD" },
"subtotalPriceFormatted": "$20.00",
"totalShippingPrice": { "amount": 10, "currencyCode": "USD" },
"totalShippingPriceFormatted": "$10.00",
"totalPrice": { "amount": 30, "currencyCode": "USD" },
"totalPriceFormatted": "$30.00",
"nextOrderDate": "2027-11-25T13:00:00.000+02:00",
"humanReadableNextOrderDate": "November 25, 2027",
"nextCycleIndex": 1,
"modifiedOrderDate": null,
"humanReadableModifiedOrderDate": null,
"modifiedCycleIndex": null,
"deliveryInterval": "MONTH",
"deliveryFrequency": 1,
"billingInterval": "MONTH",
"billingFrequency": 1,
"maxCycles": null,
"minCycles": null,
"isPrepaid": false,
"manageSubscriptionUrl": "https://shopify.com/",
"merchantSupportEmail": "example@gmail.com",
"deliveryMethod": {
"__typename": "SubscriptionDeliveryMethodShipping",
"address": {
"address1": "2515 South Broad Street",
"address2": null,
"city": "Linden",
"province": "New Jersey",
"firstName": "John",
"lastName": "Doe",
"provinceCode": "NJ",
"countryCodeV2": "US",
"zip": "07036",
"phone": null
},
"shippingOption": {
"presentmentTitle": "Standard shipping",
"title": "Standard shipping"
}
},
"paymentMethod": {
"id": "gid://shopify/CustomerPaymentMethod/2374f6125e593465b7ab15f47d971432",
"revokedAt": null,
"revokedReason": null,
"__typename": "CustomerPaymentMethod",
"instrument": {
"__typename": "CustomerCreditCard",
"brand": "visa",
"lastDigits": "4242",
"billingAddress": {
"firstName": "John",
"lastName": "Doe",
"address1": "2515 South Broad Street",
"province": "New Jersey",
"countryCode": "US",
"city": "Linden",
"zip": "07036"
}
}
},
"churnReasons": null,
"churnFreeText": null
}
}
FieldTypeDescription
contractIdstringNumeric Shopify subscription contract ID.
subscriptionCreatedAtstringISO-8601 timestamp when the contract was created.
currencyCodestringISO currency code, e.g. USD.
customerobjectCustomer details — see below.
linesCountnumberNumber of line items on the contract.
linesarrayLine items — see below.
subtotalPriceobject{ amount, currencyCode } — line subtotal before shipping.
subtotalPriceFormattedstringLocalized subtotal, e.g. $20.00.
totalShippingPriceobject{ amount, currencyCode } — shipping charge per cycle.
totalShippingPriceFormattedstringLocalized shipping total.
totalPriceobject{ amount, currencyCode } — total charged per cycle.
totalPriceFormattedstringLocalized total.
nextOrderDatestring | nullISO-8601 date of the next scheduled order, in the customer’s timezone.
humanReadableNextOrderDatestring | nullHuman-readable next order date, e.g. November 25, 2027.
nextCycleIndexnumber | nullIndex of the next billing cycle (0-based).
modifiedOrderDatestring | nullSee conditional fields.
humanReadableModifiedOrderDatestring | nullSee conditional fields.
modifiedCycleIndexnumber | nullSee conditional fields.
deliveryIntervalstringDelivery interval: DAY, WEEK, MONTH, or YEAR.
deliveryFrequencynumberNumber of intervals between deliveries (e.g. 1 = every 1 month).
billingIntervalstringBilling interval: DAY, WEEK, MONTH, or YEAR.
billingFrequencynumberNumber of intervals between charges.
maxCyclesnumber | nullMaximum number of billing cycles, if capped.
minCyclesnumber | nullMinimum number of billing cycles, if committed.
isPrepaidbooleantrue if the subscription is prepaid.
manageSubscriptionUrlstringCustomer-facing link to manage the subscription.
merchantSupportEmailstring | nullMerchant support email, if configured.
deliveryMethodobjectShopify delivery method (shipping or pickup), incl. native __typename.
paymentMethodobjectShopify customer payment method, incl. native __typename.
churnReasonsstring[] | nullSee conditional fields.
churnFreeTextstring | nullSee conditional fields.
FieldTypeDescription
idstringNumeric Shopify customer ID.
emailstringCustomer email.
firstNamestring | nullFirst name.
lastNamestring | nullLast name.
displayNamestring | nullDisplay name.
lifetimeDurationstring | nullHow long they’ve been a customer, e.g. about 2 months.
localestring | nullCustomer locale, e.g. en-US.
numberOfOrdersnumber | nullLifetime order count.
tagsstring[]Shopify customer tags.
isEmailVerifiedboolean | nullWhether the email is verified.
FieldTypeDescription
titlestringProduct title.
variantTitlestring | nullVariant title.
productIdstring | nullNumeric Shopify product ID.
variantIdstring | nullNumeric Shopify variant ID.
quantitynumberUnits per delivery.
variantImageUrlstring | nullVariant image URL.
variantImageAltTextstring | nullVariant image alt text.
lineDiscountedPricenumberFinal per-line price after discounts.
lineDiscountedPriceFormattedstringLocalized per-line price, e.g. $10.00.

Some fields are only populated for specific events; otherwise they are null:

  • modifiedOrderDate, humanReadableModifiedOrderDate, and modifiedCycleIndex are set only for order/skipped and order/unskipped, identifying the cycle that was skipped or restored.
  • churnReasons and churnFreeText are set only for subscription_contract/paused and subscription_contract/cancelled, and only when the customer provides a reason.