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:
- Enter an HTTPS endpoint URL (e.g.
https://example.com/webhooks/kaching). - 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.
Events
Section titled “Events”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. |
Request format
Section titled “Request format”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 */ }}Verifying the signature
Section titled “Verifying the signature”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:
timestampis the value of theX-Kaching-Webhook-Timestampheader, andrawBodyis the exact raw request body — the bytes as received, before any JSON parsing or re-serialization.
To verify:
- Read the
X-Kaching-Webhook-TimestampandX-Kaching-Hmac-Sha256headers. - Compute
HMAC_SHA256(signingSecret, "${timestamp}.${rawBody}")and hex-encode it. - Compare it to
X-Kaching-Hmac-Sha256using a constant-time comparison. - Reject the request if the timestamp is too old. Compare
X-Kaching-Webhook-Timestampto 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;}Responding & delivery
Section titled “Responding & delivery”- Acknowledge with any
2xxstatus (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, and5xxresponses are retried. Other4xxresponses 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.
Payload reference
Section titled “Payload reference”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 }}Top-level fields
Section titled “Top-level fields”| 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. |
customer
Section titled “customer”| 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. |
lines[]
Section titled “lines[]”| 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. |
Conditional fields
Section titled “Conditional fields”Some fields are only populated for specific events; otherwise they are null:
modifiedOrderDate,humanReadableModifiedOrderDate, andmodifiedCycleIndexare set only fororder/skippedandorder/unskipped, identifying the cycle that was skipped or restored.churnReasonsandchurnFreeTextare set only forsubscription_contract/pausedandsubscription_contract/cancelled, and only when the customer provides a reason.