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.

Topic Fires when
subscription_contract/created A new subscription contract is created.
subscription_contract/paused A contract is paused.
subscription_contract/resumed A paused contract is resumed.
subscription_contract/cancelled A contract is cancelled.
subscription_contract/upcoming_charge The pre-renewal reminder is sent, before a renewal.
billing_attempt/success A renewal payment succeeds.
billing_attempt/failure A renewal payment fails.
order/skipped An upcoming subscription order is skipped.
order/unskipped A previously skipped order is restored.

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

Headers

Header Value
X-Kaching-Topic The event topic, e.g. subscription_contract/created.
X-Kaching-Webhook-Timestamp UNIX timestamp (seconds) when the request was signed.
X-Kaching-Hmac-Sha256 HMAC-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 (200–299). 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
}
}
Field Type Description
contractId string Numeric Shopify subscription contract ID.
subscriptionCreatedAt string ISO-8601 timestamp when the contract was created.
currencyCode string ISO currency code, e.g. USD.
customer object Customer details — see below.
linesCount number Number of line items on the contract.
lines array Line items — see below.
subtotalPrice object { amount, currencyCode } — line subtotal before shipping.
subtotalPriceFormatted string Localized subtotal, e.g. $20.00.
totalShippingPrice object { amount, currencyCode } — shipping charge per cycle.
totalShippingPriceFormatted string Localized shipping total.
totalPrice object { amount, currencyCode } — total charged per cycle.
totalPriceFormatted string Localized total.
nextOrderDate string | null ISO-8601 date of the next scheduled order, in the customer’s timezone.
humanReadableNextOrderDate string | null Human-readable next order date, e.g. November 25, 2027.
nextCycleIndex number | null Index of the next billing cycle (0-based).
modifiedOrderDate string | null See conditional fields.
humanReadableModifiedOrderDate string | null See conditional fields.
modifiedCycleIndex number | null See conditional fields.
deliveryInterval string Delivery interval: DAY, WEEK, MONTH, or YEAR.
deliveryFrequency number Number of intervals between deliveries (e.g. 1 = every 1 month).
billingInterval string Billing interval: DAY, WEEK, MONTH, or YEAR.
billingFrequency number Number of intervals between charges.
maxCycles number | null Maximum number of billing cycles, if capped.
minCycles number | null Minimum number of billing cycles, if committed.
isPrepaid boolean true if the subscription is prepaid.
manageSubscriptionUrl string Customer-facing link to manage the subscription.
merchantSupportEmail string | null Merchant support email, if configured.
deliveryMethod object Shopify delivery method (shipping or pickup), incl. native __typename.
paymentMethod object Shopify customer payment method, incl. native __typename.
churnReasons string[] | null See conditional fields.
churnFreeText string | null See conditional fields.
Field Type Description
id string Numeric Shopify customer ID.
email string Customer email.
firstName string | null First name.
lastName string | null Last name.
displayName string | null Display name.
lifetimeDuration string | null How long they’ve been a customer, e.g. about 2 months.
locale string | null Customer locale, e.g. en-US.
numberOfOrders number | null Lifetime order count.
tags string[] Shopify customer tags.
isEmailVerified boolean | null Whether the email is verified.
Field Type Description
title string Product title.
variantTitle string | null Variant title.
productId string | null Numeric Shopify product ID.
variantId string | null Numeric Shopify variant ID.
quantity number Units per delivery.
variantImageUrl string | null Variant image URL.
variantImageAltText string | null Variant image alt text.
lineDiscountedPrice number Final per-line price after discounts.
lineDiscountedPriceFormatted string Localized 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.