Skip to content

Kaching Subscriptions API Documentation

Create rich subscription flows and streamline store operations on Shopify using our full-featured, enterprise-ready REST APIs.

  • A Shopify store with the Kaching Subscriptions app installed
  • A Kaching Subscriptions API key (generated from the app admin)
  • The Advanced plan or higher — the API is not available on lower plans, and requests from a shop below it are rejected with 403 Unauthorized
https://subscriptions.kachingappz.app/shop-api

All responses are JSON.

Every request must include a Bearer token in the Authorization header:

Authorization: Bearer <your-api-token>

If the header is missing, malformed, or the token is unknown, the API responds with 401 Unauthorized:

{
"error": "Unauthorized",
"message": "Invalid token"
}

The API allows 60 requests per 60 seconds per shop. Requests over the limit receive 429 Too Many Requests:

{
"toast": {
"message": "Too many requests, please try again later.",
"isError": true
}
}

GET /shop-api/analytics is the one exception: it carries its own, tighter budgets because a single call computes around twenty metrics. See Caching and rate limits.

List responses also include a throttle field reflecting the remaining Shopify GraphQL budget. When that budget drops below 1000, the API automatically delays the response by ~3 seconds to avoid tripping Shopify-side limits — design your integration to tolerate that latency.

Confirm a token works and learn which shop it belongs to.

GET /shop-api

Returns the shop attached to the API token. Useful as a lightweight health check to verify a token is valid.

Example

Terminal window
curl "https://subscriptions.kachingappz.app/shop-api" \
-H "Authorization: Bearer <your-api-token>"

Response (200)

{
"shop": "mystore.myshopify.com"
}

Read the calculated metrics the Kaching admin shows, rather than re-deriving them from contracts and webhooks.

GET /shop-api/analytics

Returns the metrics the Kaching admin shows on its Analytics and Billing health pages — MRR, churn rate, retention, subscription counts and payment success rates — for a preset period or a custom day range.

These are the app’s own calculated figures, not raw data to re-derive: the endpoint reads them through the very same services that render those pages, so a number here and the number on the merchant’s screen cannot disagree. See Matching the admin exactly for the three things that most often make them look different.

Query parameters

Parameter Type Required Default Description
period string No LAST_30_DAYS A preset period (below). Ignored when from and to are given.
from string No — First day of a custom range, YYYY-MM-DD, read in tz. Send together with to.
to string No — Last day of the range, inclusive. On or after from, and not after today in tz.
tz string No the shop’s IANA timezone name, e.g. America/New_York. Decides which instants each day covers.

Accepted period values: TODAY, YESTERDAY, LAST_24_HOURS, LAST_7_DAYS, LAST_30_DAYS, LAST_90_DAYS, LAST_365_DAYS, LAST_MONTH (previous calendar month), LAST_12_MONTHS, LAST_YEAR (previous calendar year), ALL_TIME.

There is no maximum range. period=CUSTOM is also accepted alongside from and to, so a URL copied from the admin’s own address bar works unchanged.

Example

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/analytics" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "from=2026-09-01" \
--data-urlencode "to=2026-09-30" \
--data-urlencode "tz=America/New_York"

Response (200)

{
"shop": "example.myshopify.com",
"currency": "USD",
"timezone": "America/New_York",
"period": null,
"customRange": { "from": "2026-09-01", "to": "2026-09-30" },
"range": {
"from": "2026-09-01T04:00:00.000Z",
"to": "2026-10-01T03:59:59.999Z"
},
"failedPaymentsCountAsChurn": false,
"subscriptions": {
"activeCount": 259,
"pausedCount": 17,
"cancelledCount": 48,
"newCount": 296,
"churnedCount": 74,
"reactivatedCount": 12,
"netChange": 211,
"churnRate": 24.71,
"averageRetentionPeriodDays": 121,
"averageActiveSubscriptionAgeDays": 197
},
"revenue": {
"mrr": 31916.2,
"expectedCollectionsNext30Days": 25626.32,
"upcomingRevenue": [{ "date": "2026-10-16", "revenue": 583.85 }],
"arpu": 61.33,
"averageOrderValue": 48.9,
"averageOrdersPerSubscription": 4.2,
"averageLifetimeRevenuePerSubscription": 205.4,
"firstSubscriptionOrdersCount": 96,
"recurringOrdersCount": 1104,
"firstSubscriptionOrdersRevenue": 4512.0,
"recurringOrdersRevenue": 51993.2,
"totalSubscriptionRevenue": 56505.2,
"subscriptionTakeRate": 0.31,
"subscriptionRevenueShare": 0.72
},
"billingHealth": {
"successRate": 94.2,
"firstTrySuccessRate": 88.1,
"failureRate": 5.8,
"perCycleSuccessRate": 97.4,
"perCycleFirstTrySuccessRate": 90.2,
"isPerCycleRatePreferred": true,
"totalBillingAttemptsCount": 1210,
"successfulBillingAttemptsCount": 1140,
"failedBillingAttemptsCount": 70,
"firstTrySuccessCount": 1066,
"recoveredBillingAttemptsCount": 52,
"recoveredPaymentsValue": 2480.0,
"lostBillingAttemptsCount": 18,
"successCyclesCount": 1140,
"lostCyclesCount": 31,
"resolvedCyclesCount": 1171,
"subscriptionsInDunningCount": 24,
"failureReasons": {
"rows": [{ "reason": "insufficient_funds", "count": 31, "percentage": 44.3 }],
"truncated": false,
"totalDistinctReasons": 6
}
}
}
Field Type Description
shop string The shop domain the token belongs to.
currency string The shop’s currency. Every money figure is in it.
timezone string The zone the window was read in — your tz, or the shop’s.
period string|null The preset read, or null when a custom range was.
customRange object|null { from, to } inclusive days, or null when a preset was read.
range object The instants the window actually covers, inclusive, as ISO-8601 UTC.
failedPaymentsCountAsChurn boolean The shop’s churn setting — see below.
Field Type Description
activeCount number|null Subscriptions counted as active right now — not over the window. Includes or excludes contracts in dunning per the churn setting.
pausedCount number|null Contracts currently paused. Now, not over the window.
cancelledCount number|null Contracts currently cancelled. Now, not over the window.
newCount number|null Subscriptions that became active during the window, summed per day. One that starts and churns the same day is excluded.
churnedCount number|null Subscriptions that became inactive during the window, summed per day. One that churns and returns the same day is excluded.
reactivatedCount number|null Previously inactive subscriptions that became active again during the window.
netChange number|null Last recorded active count in the window minus the first. Days without a snapshot are skipped, so for a shop younger than the window this spans only the recorded days.
churnRate number|null Percent 0–100: (starting + new − ending) / (starting + new). null unless the window both begins and ends with a recorded snapshot — see the caution below.
averageRetentionPeriodDays number|null Average days a subscription stayed active before churning, over those that churned in the window. null when none did.
averageActiveSubscriptionAgeDays number|null Average age in days of currently-active subscriptions, from creation to today.
Field Type Description
mrr number|null Monthly Recurring Revenue: every active contract’s price normalised to a month. Always active-only.
expectedCollectionsNext30Days number|null Revenue expected from orders scheduled in the next 30 days. Forward-looking: it ignores period.
upcomingRevenue array|null { date, revenue } per upcoming day — the daily breakdown of the figure above. Also ignores period.
arpu number|null Average revenue per unique customer from subscription orders in the last 30 days. Ignores period.
averageOrderValue number|null Average value across all subscription orders, initial and recurring. All time.
averageOrdersPerSubscription number|null Average successful orders per subscription over its lifetime. All time.
averageLifetimeRevenuePerSubscription number|null Average total revenue per subscription over its lifetime. All time.
firstSubscriptionOrdersCount number|null Initial orders from subscriptions created during the window.
recurringOrdersCount number|null Recurring subscription orders during the window.
firstSubscriptionOrdersRevenue number|null Revenue from those initial orders.
recurringOrdersRevenue number|null Revenue from those recurring orders.
totalSubscriptionRevenue number|null The two above added. Includes tax and shipping, subtracts discounts; refunds are not subtracted.
subscriptionTakeRate number|null Ratio 0–1: first-subscription-order revenue against that plus one-time revenue.
subscriptionRevenueShare number|null Ratio 0–1: all subscription revenue against all revenue.

The last two are the only figures read from Shopify rather than from Kaching’s own records, so they are also the only ones a Shopify rate limit can turn into null on its own. Every other field in the response is unaffected when that happens.

Two readings of the same window, deliberately both returned.

Field Type Description
successRate number|null Percent 0–100, per attempt: successful billing attempts over all attempts in the window.
firstTrySuccessRate number|null Percent 0–100: attempts that succeeded first try, over all attempts.
failureRate number|null Percent 0–100: failed attempts over all attempts.
perCycleSuccessRate number|null Percent 0–100, per cycle: cycles that ended up paid, over cycles that resolved. Cycles still retrying are in neither.
perCycleFirstTrySuccessRate number|null Percent 0–100: cycles paid on the first attempt, over resolved cycles.
isPerCycleRatePreferred boolean Which of the two bases the merchant’s own Billing health page is currently showing.
totalBillingAttemptsCount number|null Billing attempts in the window.
successfulBillingAttemptsCount number|null Of those, successful.
failedBillingAttemptsCount number|null Of those, failed.
firstTrySuccessCount number|null Of those, successful on the first attempt.
recoveredBillingAttemptsCount number|null Payments that failed and were later recovered by dunning.
recoveredPaymentsValue number|null Their total value, in currency.
lostBillingAttemptsCount number|null Attempts dunning gave up on.
successCyclesCount number|null Cycles that resolved into a paid order in the window.
lostCyclesCount number|null Cycles dunning gave up on, deduplicated.
resolvedCyclesCount number|null successCyclesCount + lostCyclesCount — the per-cycle denominator.
subscriptionsInDunningCount number|null Subscriptions currently in dunning. Now, not over the window.
failureReasons object|null { rows: [{ reason, count, percentage }], truncated, totalDistinctReasons } — top 20 reasons by count.

Why the two rates differ so much. Every dunning retry writes its own failure, so a cycle that failed four times and then paid contributes one success and four failures to the per-attempt rate. The harder a shop retries, the worse that rate looks. perCycleSuccessRate counts each cycle once, and is usually what a merchant means by “how many of my charges go through”.

Merchants choose, in the Kaching admin under Settings → General, whether a subscription whose payment failed and never recovered counts as churned. failedPaymentsCountAsChurn reports that choice. It cannot be overridden per request — following the saved setting is what keeps these figures equal to the ones on the merchant’s screen.

Setting What counts as a live subscription
false Active and failed. A failed payment is still in dunning and can recover, so it stays alive.
true Active only. A failure never followed by a return to active churns on the day the payment failed.

Fields that follow it: subscriptions.activeCount, averageActiveSubscriptionAgeDays, churnRate and netChange.

Fields that never follow it: revenue.mrr (always active-only), subscriptions.newCount, churnedCount, reactivatedCount, averageRetentionPeriodDays, pausedCount, cancelledCount, and everything under billingHealth.

A successful response is cached for 5 minutes per shop, window, timezone and setting, and says so:

Cache-Control: private, max-age=300
X-Kaching-Analytics-Cache: hit

Changing the failed-payments setting takes effect immediately — it is part of the cache key, not merely reported in the body.

This endpoint carries its own limits in place of the global 60/60s, because one call computes around twenty separate metrics:

Bucket Budget Counts
Requests 30 per 60 seconds Every request.
Cold computations 10 per 300 seconds Only requests that miss the 5-minute response cache.

A job polling one window never touches the second budget — after the first call its requests come from cache. The second budget bounds a caller walking many different windows. Refusals carry Retry-After in seconds.

  • Timezone is the usual reason two numbers disagree. The admin’s Analytics page reads the window in the browser’s timezone; this endpoint defaults to the shop’s. To reproduce a screen exactly, send the tz that browser was in.
  • Some figures ignore period by design. expectedCollectionsNext30Days and upcomingRevenue look 30 days forward; arpu is always the last 30 days; averageOrderValue, averageOrdersPerSubscription and averageLifetimeRevenuePerSubscription are all-time; and activeCount, pausedCount, cancelledCount and subscriptionsInDunningCount are counts of now, not of the window.
  • ALL_TIME and LAST_24_HOURS have no equivalent in the admin. Its date filter does not offer them, so those two cannot be compared against a screen. Every other period, and any custom range, can.

Unlike the admin — which quietly falls back to “Last 7 days” for a hand-edited URL — this endpoint always tells you when a parameter is wrong, so an integration never books a week’s figures as the quarter it asked for.

Request Response
from without to (or vice versa) 400 { "error": "from and to must be given together" }
to before from 400 { "error": "The start date must be on or before the end date" }
A day that is not a real date 400 { "error": "Not a real calendar date" }
to after today in tz 400 { "error": "to must not be after today (2026-10-15 in UTC)" }
An unknown period 400 listing the accepted values
period=CUSTOM without both days 400 { "error": "period=CUSTOM needs both from and to" }
A tz that is not an IANA zone 400 { "error": "tz must be a valid IANA timezone" }

A single metric that cannot be read is not an error — it comes back as null with the rest of the response intact.


Read subscription contracts and change their state.

GET /shop-api/contracts

Returns a paginated list of subscription contracts for the authenticated shop.

Query parameters

Name Type Required Default Description
cursor string No — Opaque pagination cursor from the previous response’s cursor field.
query string No status:ACTIVE OR status:PAUSED OR status:FAILED OR status:CANCELLED Shopify GraphQL query syntax, e.g. status:ACTIVE, status:PAUSED.
email string No — Customer email (case-insensitive exact match). Narrows the result to that customer’s contracts. Combines with query via AND. Unknown email returns an empty data array.
customerId string No — Shopify customer ID — either a numeric ID (e.g. 258369147) or a full GID (gid://shopify/Customer/258369147). Narrows the result to that customer’s contracts. Combines with query via AND. Unknown customer returns an empty data array. Takes precedence over email if both are supplied.
orderId string No — Shopify order ID — either a numeric ID (e.g. 369147258) or a full GID (gid://shopify/Order/369147258). Narrows the result to the contract(s) attached to that order. Combines with query via AND. Unknown or non-subscription orders return an empty data array. Takes precedence over customerId and email if multiple are supplied.

Each page returns up to 50 contracts.

Example — fetch all active contracts

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "query=status:ACTIVE"

Example — paginate

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "query=status:ACTIVE" \
--data-urlencode "cursor=eyJsYXN0X2lkIjoi..."

Loop until hasNextPage is false, passing the previous response’s cursor on each iteration.

Example — fetch a single contract by ID

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "query=id:123456789"

Example — fetch a specific customer’s contracts by email

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "email=sarah@example.com"

Combine with query to narrow further, e.g. only that customer’s cancelled contracts:

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "email=sarah@example.com" \
--data-urlencode "query=status:CANCELLED"

Example — fetch a specific customer’s contracts by customer ID

Customer ID is preferred over email when available — it’s stable across email changes. Accepts either the numeric ID or the full Shopify GID.

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "customerId=258369147"
Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "customerId=gid://shopify/Customer/258369147"

Example — fetch a contract by orderId

Use orderId when you have a Shopify order in hand and want to find the subscription contract(s) it was generated by. Accepts either the numeric ID or the full Shopify GID.

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "orderId=369147258"
Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "orderId=gid://shopify/Order/369147258"

If multiple of orderId, customerId, and email are provided, priority is orderId > customerId > email; the lower-priority parameters are ignored.

Response (200)

{
"data": [
{
"node": {
"id": "gid://shopify/SubscriptionContract/123456789",
"status": "ACTIVE",
"currencyCode": "USD",
"lastPaymentStatus": "SUCCEEDED",
"revisionId": "gid://shopify/SubscriptionContractRevision/987654321",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-02-10T14:22:00Z",
"deliveryPolicy": { "interval": "MONTH", "intervalCount": 1 },
"billingPolicy": {
"interval": "MONTH",
"intervalCount": 1,
"maxCycles": null,
"minCycles": null
},
"deliveryPrice": { "amount": "5.99", "currencyCode": "USD" },
"deliveryMethod": {
"__typename": "SubscriptionDeliveryMethodShipping",
"address": { "countryCodeV2": "US", "city": "San Francisco" }
},
"linesCount": { "count": 2 },
"lines": {
"edges": [
{
"node": {
"id": "gid://shopify/SubscriptionLine/111222333",
"title": "Product Title",
"variantTitle": "Variant Title",
"productId": "gid://shopify/Product/444555666",
"variantId": "gid://shopify/ProductVariant/777888999",
"quantity": 2,
"lineDiscountedPrice": {
"amount": "47.98",
"currencyCode": "USD"
},
"pricingPolicy": {
"basePrice": { "amount": "25.99", "currencyCode": "USD" },
"cycleDiscounts": [
{
"adjustmentType": "PERCENTAGE",
"afterCycle": 0,
"computedPrice": {
"amount": "23.99",
"currencyCode": "USD"
},
"adjustmentValue": { "percentage": 8.0 }
}
]
},
"discountAllocations": []
}
}
],
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"endCursor": null,
"startCursor": null
}
},
"discounts": { "edges": [] },
"customer": {
"id": "gid://shopify/Customer/258369147",
"displayName": "Sarah Johnson",
"defaultEmailAddress": { "emailAddress": "sarah@example.com" }
},
"orders": {
"edges": [{ "node": { "id": "gid://shopify/Order/369147258" } }],
"pageInfo": {
"hasNextPage": false,
"hasPreviousPage": false,
"endCursor": null,
"startCursor": null
}
}
}
}
],
"hasNextPage": true,
"cursor": "eyJsYXN0X2lkIjoiZ2lkOi8vc2hvcGlmeS...",
"throttle": 1850
}

See the Contract object reference below for field details.


POST /shop-api/contracts/pause

Transitions an ACTIVE contract to PAUSED. No further billing cycles run until the contract is resumed.

Request body

Field Type Required Description
contractId string Yes Either a Shopify GID (gid://shopify/SubscriptionContract/123) or the bare numeric ID.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/pause" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{"contractId":"gid://shopify/SubscriptionContract/123456789"}'

Response (200)

{ "success": true }

POST /shop-api/contracts/resume

Transitions a PAUSED contract back to ACTIVE. Billing resumes on the contract’s configured schedule.

Request body

Field Type Required Description
contractId string Yes Shopify GID or numeric ID of the contract.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/resume" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{"contractId":"gid://shopify/SubscriptionContract/123456789"}'

Response (200)

{ "success": true }

POST /shop-api/contracts/cancel

Transitions a contract to CANCELLED.

Request body

Field Type Required Description
contractId string Yes Shopify GID or numeric ID of the contract.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/cancel" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{"contractId":"gid://shopify/SubscriptionContract/123456789"}'

Response (200)

{ "success": true }

POST /shop-api/contracts/reactivate

Brings a cancelled subscription back.

The new contract keeps the customer, payment method, lines, prices, discounts, delivery method and billing interval of the old one. Anything a Kaching streak had placed on the old contract (a free gift line, a streak discount) is left behind; the new contract starts earning its own.

Request body

Field Type Required Description
contractId string Yes The cancelled contract: Shopify GID or numeric ID.
reactivationDate string No YYYY-MM-DD in the shop’s timezone — the day the new contract first bills, at 11:00 shop time. Tomorrow to 30 days out. Defaults to tomorrow in the shop’s timezone.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/reactivate" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{
"contractId": "gid://shopify/SubscriptionContract/123456789",
"reactivationDate": "2026-10-20"
}'

Response (200)

{
"success": true,
"contract": {
"id": "gid://shopify/SubscriptionContract/19001234567",
"reactivatedFromId": "gid://shopify/SubscriptionContract/123456789"
}
}

List contracts then shows the new contract as ACTIVE and the old one as CANCELLED.

A contract gets one successor. A second reactivation of the same contract is refused with 409, so that a customer is never left paying for two live subscriptions:

{ "error": "Subscription has already been reactivated" }

A contract that is not cancelled is refused with 409 too ({ "error": "Subscription is not cancelled" }), and one that cannot be rebuilt as it stands — no payment method, a revoked payment method, a prepaid plan, a line whose product no longer exists, or a currency the store no longer sells in — with 422 and a message naming the reason.


POST /shop-api/contracts/add-discount

Applies a discount to every line of a contract. The value is supplied inline, so you do not need to create a Shopify discount code first. The discount shows up in pricingPolicy.cycleDiscounts and discountAllocations the next time you list contracts.

Request body

Field Type Required Description
contractId string|number Yes Shopify GID or numeric ID of the contract.
discountType string Yes PERCENTAGE or FIXED_AMOUNT.
value number Yes PERCENTAGE: a whole number from 1 to 100. FIXED_AMOUNT: a positive amount, at most 2 decimals, up to 100000.
title string Yes 2–255 characters. Shown to the customer on the order.
recurringCycleLimit number No How many upcoming orders the discount applies to (1–120). Omit it, or send null, to apply the discount to every future order.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/add-discount" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{
"contractId": "gid://shopify/SubscriptionContract/123456789",
"discountType": "PERCENTAGE",
"value": 15,
"title": "15% off your next 3 orders",
"recurringCycleLimit": 3
}'

Response (200)

{
"success": true,
"discount": {
"id": "gid://shopify/SubscriptionManualDiscount/12345678",
"title": "15% off your next 3 orders",
"type": "PERCENTAGE",
"value": 15,
"currencyCode": "USD",
"recurringCycleLimit": 3
}
}

Every call applies — repeat calls stack. There is no deduplication: two requests leave two discounts on the contract, whether or not they are identical. Stacking is deliberate, so that you can layer a second offer on a contract that already carries one.


Change what a subscription contains. These endpoints alter what the customer receives and pays, so read the notes at the end of the group before wiring them up.

POST /shop-api/contracts/add-line-item

Adds a product variant to a contract. The new line rides the contract’s existing billing schedule and appears in lines the next time you list contracts.

Request body

Field Type Required Description
contractId string|number Yes Shopify GID or numeric ID of the contract.
variantId string|number Yes Shopify GID (gid://shopify/ProductVariant/123) or numeric ID of the variant to add.
quantity number Yes Whole number from 1 to 1000000.
price number No Per-unit price in the contract’s currency: 0 or more, at most 2 decimals, up to 1000000. Omit it to price the line at the variant’s price for the contract’s delivery country.
currencyCode string No Three-letter currency code. A check, not a conversion: the request is refused if it differs from the contract’s currencyCode.

How the line is priced

  • With price — the line is contracted at that price on every future order. The contract’s selling plan cannot re-price it later.
  • Without price — the line is priced at the variant’s price for the market the contract ships to, in the contract’s currency. If the variant has no price in that currency, the request is refused with 422 and nothing is added.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/add-line-item" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{
"contractId": "123456789",
"variantId": "44123456789",
"quantity": 2,
"price": 25.99,
"currencyCode": "USD"
}'

Response (200)

{
"success": true,
"line": {
"id": "gid://shopify/SubscriptionLine/0f1dcb8f-e608-4c3f-b153-dc77897d39e9",
"variantId": "gid://shopify/ProductVariant/44123456789",
"quantity": 2,
"price": 25.99,
"currencyCode": "USD",
"priceSource": "REQUEST"
}
}

priceSource is REQUEST when the price is the one you sent, and CONTEXTUAL when it was taken from the contract’s market.


POST /shop-api/contracts/update-line-item

Changes the variant, quantity and/or price of an existing line. Send at least one of variantId, quantity and price.

Request body

Field Type Required Description
contractId string|number Yes Shopify GID or numeric ID of the contract.
lineId string Yes The line’s id from list contracts — the full gid://shopify/SubscriptionLine/… GID or the part after the last /.
variantId string|number No Move the line to this variant.
quantity number No New quantity, a whole number from 1 to 1000000.
price number No New per-unit price in the contract’s currency, with the same rules as for adding a line.
currencyCode string No Checked against the contract’s currency, as for adding a line.

What each field changes

  • quantity — only the quantity. The line’s price and discount schedule are left as they are.
  • variantId — the line moves to the new variant at the price it already has. A variant swap never changes what the customer pays; send price as well if it should.
  • price — the line is contracted at the new price on every future order, replacing any discount schedule it had.

Example — swap the variant and change the quantity

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/update-line-item" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{
"contractId": "123456789",
"lineId": "gid://shopify/SubscriptionLine/0f1dcb8f-e608-4c3f-b153-dc77897d39e9",
"variantId": "44987654321",
"quantity": 3
}'

Response (200)

{
"success": true,
"line": {
"id": "gid://shopify/SubscriptionLine/0f1dcb8f-e608-4c3f-b153-dc77897d39e9",
"variantId": "gid://shopify/ProductVariant/44987654321",
"quantity": 3,
"price": 25.99,
"currencyCode": "USD",
"priceSource": "REQUEST"
},
"removedDiscounts": []
}

Each entry in removedDiscounts is { "discountId": "gid://shopify/SubscriptionManualDiscount/…", "reason": "quantity-invalidated" }.


POST /shop-api/contracts/remove-line-item

Removes a line from a contract. The contract’s last line cannot be removed — to stop a subscription, cancel it instead.

Request body

Field Type Required Description
contractId string|number Yes Shopify GID or numeric ID of the contract.
lineId string Yes The line’s id from list contracts.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/remove-line-item" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{
"contractId": "123456789",
"lineId": "gid://shopify/SubscriptionLine/0f1dcb8f-e608-4c3f-b153-dc77897d39e9"
}'

Response (200)

{
"success": true,
"lineId": "gid://shopify/SubscriptionLine/0f1dcb8f-e608-4c3f-b153-dc77897d39e9"
}

  • Only ACTIVE, PAUSED and FAILED contracts can be changed. Others are refused with 422, and so is a contract with more than 50 line items.
  • Changes apply to every future order, not just the next one.
  • Every call applies — repeat calls are not deduplicated. Two identical add-line-item requests add two lines. A request that timed out may still have been applied, so read list contracts before retrying.
  • Each call is its own change. To replace one product with another, either swap the variant with update-line-item, or add the new line and then remove the old one — and if you do the latter, check the contract afterwards, since a failure in between leaves both lines or neither.
  • Lines given as a streak reward cannot be changed or removed (403).
  • If the contract is being changed by something else at that moment, the request is refused with 409. Nothing was applied, so retry after a short wait.
  • The merchant’s contract-update guard applies. If the shop has it turned on, a change that takes the contract’s total above both $100 USD and twice the value of its first order is refused with 422.
  • Every change appears on the contract’s timeline in the Kaching admin as a “via API” entry.

Read and adjust when a contract is charged, one cycle at a time.

GET /shop-api/contracts/billing-cycles

Returns the billing cycles of a single contract, including the cycleIndex that the skip, unskip and reschedule endpoints below take. A billing cycle is the unit a customer is charged on: it has a billing date, a window that date must stay inside, a status, and a skipped flag.

Query parameters

Parameter Type Required Default Description
contractId string|number Yes — Shopify GID or numeric ID of the contract.
first number No 10 How many cycles to return, 1–10.
fromCycleIndex number No — Start of an explicit cycle range. Must be sent together with toCycleIndex.
toCycleIndex number No — End of that range, inclusive. Must be greater than or equal to fromCycleIndex.

Without a range, the window starts at now in the shop’s timezone and runs forward, so only upcoming cycles are returned. Supply the range to see cycles that have already been billed, are already skipped, or whose billing date has passed. A range reaching past the contract’s last cycle is refused with 404 rather than truncated, so ask for one you know exists.

Example

Terminal window
curl -G "https://subscriptions.kachingappz.app/shop-api/contracts/billing-cycles" \
-H "Authorization: Bearer <your-api-token>" \
--data-urlencode "contractId=123456789" \
--data-urlencode "first=3"

Response (200)

{
"contractId": "gid://shopify/SubscriptionContract/123456789",
"subscriptionContractStatus": "ACTIVE",
"billingCycles": [
{
"cycleIndex": 4,
"billingAttemptExpectedDate": "2026-02-15T10:00:00Z",
"cycleStartAt": "2026-01-15T00:00:00Z",
"cycleEndAt": "2026-02-15T00:00:00Z",
"skipped": false,
"status": "UNBILLED",
"subscriptionContractStatus": "ACTIVE",
"billingAttempts": { "nodes": [] },
"sourceContract": {
"deliveryPolicy": { "interval": "MONTH", "intervalCount": 1 },
"lines": { "nodes": [] }
}
}
]
}

subscriptionContractStatus is the contract’s status; status on each cycle is the cycle’s (UNBILLED, BILLED or CANCELLED). A cancelled contract still reports projected UNBILLED cycles, so check the contract’s status before reading the list as a schedule.


POST /shop-api/contracts/billing-cycles/skip

Skips one upcoming cycle: the customer is not charged and no order is created for it. The schedule is not delayed — every later cycle keeps its own date. To move a delivery rather than drop it, reschedule it instead.

Request body

Field Type Required Description
contractId string|number Yes Shopify GID or numeric ID of the contract.
cycleIndex number Yes The cycle to skip, from get billing cycles.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/billing-cycles/skip" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{"contractId":"123456789","cycleIndex":4}'

Response (200)

{
"success": true,
"cycleIndex": 4,
"skipped": true,
"changed": true,
"billingAttemptExpectedDate": "2026-02-15T10:00:00Z"
}

A cycle that is already skipped is not an error: the response is 200 with "changed": false, and nothing is sent to Shopify. The endpoint is safe to repeat.


POST /shop-api/contracts/billing-cycles/unskip

Puts a skipped cycle back on the schedule, so the customer is charged for it again. Same request body as skip.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/billing-cycles/unskip" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{"contractId":"123456789","cycleIndex":4}'

Response (200)

{
"success": true,
"cycleIndex": 4,
"skipped": false,
"changed": true,
"billingAttemptExpectedDate": "2026-02-15T10:00:00Z"
}

A cycle that is not skipped returns 200 with "changed": false.


POST /shop-api/contracts/billing-cycles/reschedule

Moves one cycle’s billing date. Only that cycle moves — later cycles keep their dates, so this is a one-off shift rather than a change of cadence. Call it once per cycle to move several.

Request body

Field Type Required Description
contractId string|number Yes Shopify GID or numeric ID of the contract.
cycleIndex number Yes The cycle to move.
billingDate string Yes YYYY-MM-DD, or a full ISO 8601 date-time. A bare date is read at the start of that day in the shop’s timezone.

billingDate must be later than today in the shop’s timezone, and no other cycle of the same contract may already sit on that day.

Example

Terminal window
curl -X POST "https://subscriptions.kachingappz.app/shop-api/contracts/billing-cycles/reschedule" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{"contractId":"123456789","cycleIndex":4,"billingDate":"2026-03-02"}'

Response (200)

{
"success": true,
"cycleIndex": 4,
"changed": true,
"billingAttemptExpectedDate": "2026-03-02T00:00:00-05:00"
}

The applied date can differ from the one you asked for. Shopify validates billingDate against the cycle’s own window, and derives that window from the previous cycle’s current billing date. Month-end anchoring is the usual cause: re-anchoring a monthly contract to the 30th puts February on the 28th, which shortens the window. When the date has to be adjusted, the response carries both:

{
"success": true,
"cycleIndex": 4,
"changed": true,
"billingAttemptExpectedDate": "2026-02-28T00:00:00-05:00",
"requestedBillingDate": "2026-03-02T00:00:00-05:00"
}

billingAttemptExpectedDate is always the date that took effect — show that one to operators. requestedBillingDate is present only when the two differ.


  • cycleIndex is 1-based, and there is no cycle 0. Cycle 1 is the first recurring charge: its window opens when the contract is created and it bills one billing interval later. The signup/checkout order is not a billing cycle, so the Nth recurring charge is cycle N. Always take the number from get billing cycles rather than computing it.
  • Only ACTIVE, PAUSED and FAILED contracts can be changed (422 otherwise). PAUSED is allowed on purpose: a paused contract still has projected cycles, and the date it would bill on remains meaningful.
  • A cycle that has already been billed cannot be changed (409), and neither can one whose billing is already in progress.
  • Unskip before rescheduling. A skipped cycle has no billing date to move, so a reschedule of one is refused with 409.
  • Skipping and unskipping are safe to repeat; rescheduling is too, since it sets an absolute date rather than shifting by an offset.
  • Scheduled charges follow the change. A charge already queued for a cycle you skip is abandoned, and one queued for a cycle you move is re-queued for the new date — so neither a double charge nor a missed charge results.
  • Every change appears on the contract’s timeline in the Kaching admin.

Field Type Description
id string Shopify GID for the contract.
status string One of ACTIVE, PAUSED, FAILED, EXPIRED, CANCELLED, STALE.
currencyCode string ISO currency code for the contract.
lastPaymentStatus string SUCCEEDED or FAILED.
revisionId string Opaque identifier for the current revision. Use to detect contract changes.
createdAt string ISO-8601 timestamp.
updatedAt string ISO-8601 timestamp.
deliveryPolicy object { interval, intervalCount } — how often deliveries happen.
billingPolicy object { interval, intervalCount, maxCycles, minCycles } — billing cadence and limits.
deliveryPrice object { amount, currencyCode } — shipping/delivery charge per cycle.
deliveryMethod object Delivery method, e.g. SubscriptionDeliveryMethodShipping with an address.
linesCount object { count } — total number of line items on the contract.
lines object Connection of line items — see below.
discounts object Connection of contract-level discount allocations.
customer object { id, displayName, defaultEmailAddress.emailAddress }.
orders object Connection of orders generated by this contract.

interval values: DAY, WEEK, MONTH, YEAR.

Field Type Description
id string Shopify GID for the line.
title string Product title.
variantTitle string Variant title.
productId string Shopify product GID.
variantId string Shopify variant GID.
quantity number Number of units per delivery.
lineDiscountedPrice object { amount, currencyCode } — final per-line price after discounts.
pricingPolicy object { basePrice, cycleDiscounts[] } — base price and per-cycle adjustments.
discountAllocations array Line-level discount allocations — see the note below.

Inside discountAllocations[].discount, the type field is Shopify’s DiscountType — MANUAL, CODE_DISCOUNT or AUTOMATIC_DISCOUNT — describing how the discount was applied. It is not the percentage/fixed-amount distinction. For that, read discount.value ({ percentage } or { amount, appliesOnEachItem }), or pricingPolicy.cycleDiscounts[].adjustmentType.

Cycle discount fields (pricingPolicy.cycleDiscounts[])

Section titled “Cycle discount fields (pricingPolicy.cycleDiscounts[])”
Field Type Description
adjustmentType string PERCENTAGE, FIXED_AMOUNT, or PRICE.
afterCycle number Cycle index after which the discount applies (0 = starting with cycle 1).
computedPrice object { amount, currencyCode } — resolved price once the discount applies.
adjustmentValue object Either { percentage } or { amount, currencyCode }, depending on type.

Connections (lines, orders, top-level list) return pageInfo:

Field Type Description
hasNextPage boolean true if more pages exist.
hasPreviousPage boolean true if previous pages exist.
endCursor string Cursor for the last item in the current page.
startCursor string Cursor for the first item in the current page.
Status When Example body
400 Missing or malformed request body (e.g. no contractId) or invalid query parameter (e.g. email is not a valid address, or an analytics date range that is reversed or not a real date) { "error": "Missing contractId" }
401 Missing, malformed, or unknown Bearer token { "error": "Unauthorized", "message": "Invalid token" }
403 The shop is not on the Advanced plan or higher { "error": "Unauthorized" }
403 The line is a streak reward (line item endpoints) { "error": "This line item is a streak reward and cannot be changed" }
404 No contract with that ID on this shop, no such line on the contract, or no such variant on this shop { "error": "Line item not found on this subscription contract" }
409 The contract is being changed by something else; nothing was applied (line item endpoints) { "error": "The subscription is being updated, please try again" }
409 The billing cycle’s state or the requested date forbids the change (billing cycle endpoints) { "error": "This billing cycle has already been billed" }
409 The contract is not cancelled, or has already been reactivated (reactivate) { "error": "Subscription has already been reactivated" }
422 Shopify rejected the change, or the contract cannot be modified (add-discount, line item endpoints) { "error": "Cannot remove the last line item of a subscription contract" }
422 The contract cannot be reactivated as it stands (reactivate) { "error": "Payment method has been revoked. Please add new payment method" }
429 Rate limit exceeded (60 requests / 60 seconds per shop; different for analytics) { "toast": { "message": "Too many requests, please try again later.", "isError": true } }
500 Upstream Shopify error or mutation failure { "error": "Failed to pause subscription contract" }
503 The contract’s status could not be read from Shopify. Nothing was changed, so the request can be retried { "error": "Couldn't read the subscription's status from Shopify, nothing was changed. Please try again" }

Mutation endpoints (pause, resume, cancel, reactivate, add-discount, add-line-item, update-line-item, remove-line-item, and the billing-cycles/skip, billing-cycles/unskip and billing-cycles/reschedule endpoints) surface Shopify-side validation messages in the error field when Shopify returns userErrors, so prefer showing the raw message to operators rather than masking it.