GET A FREE STORE AUDITFREE AUDIT

SEP 18, 2026 · UPDATED OCT 5, 2026 · 7 MIN READ

Subscription billing edge cases that lose customers

A failed renewal is not one event. Shopify reports more than sixty distinct reasons, and the right response to a declined card is nothing like the right response to an out-of-stock line.

Blue and white cartons stacked on a small white pallet in rising columns like a bar chart, with a hand pointing at the tallest

Read the error code before you retry. Shopify’s SubscriptionBillingAttemptErrorCode enum reports more than sixty distinct failure reasons, and they fall into three groups that need three different responses: transient failures that are safe to retry unchanged, customer-action failures where retrying is pointless until the customer updates something, and merchant-side failures that are your operational problem and not a payment problem at all. A retry schedule that treats all three the same wastes attempts on the ones it cannot fix and annoys the customers it can.

IN SHORT

  • Shopify’s SubscriptionBillingAttemptErrorCode enum distinguishes more than sixty failure reasons, including INSUFFICIENT_FUNDS, EXPIRED_CARD, AUTHENTICATION_REQUIRED, INSUFFICIENT_INVENTORY and PAYMENT_PROVIDER_IS_NOT_ENABLED.
  • Retrying is only sensible for codes that can change on their own. Shopify documents TRANSIENT_ERROR as “Transient error, try again later”, and that is the clearest example.
  • EXPIRED_CARD, INVALID_PAYMENT_METHOD and PAYMENT_METHOD_NOT_FOUND will fail identically on every retry until the customer supplies a new payment method.
  • customerPaymentMethodSendUpdateEmail “Sends an email to a customer containing a secure link to update a specific vaulted payment method”, and only its from and bcc fields are customizable.
  • INSUFFICIENT_INVENTORY and INVENTORY_ALLOCATIONS_NOT_FOUND are billing failures caused by your operations, not by the customer’s bank.
  • subscriptionBillingAttemptCreate takes an idempotencyKey so a retried request cannot charge the customer twice.
  • The SUBSCRIPTION_BILLING_ATTEMPTS_FAILURE webhook is what you build the recovery flow on; SUBSCRIPTION_BILLING_ATTEMPTS_CHALLENGED is a separate topic for 3D Secure challenges.

Three kinds of failure, not one

The single most common design error in subscription recovery is a retry schedule with no branching: attempt, wait three days, attempt, wait three days, attempt, cancel. It is easy to build and it is wrong, because it applies a strategy that only works for one class of failure to all of them.

Shopify’s billing attempt error codes make the classes obvious if you read them. Grouped by what could possibly change between now and a retry:

  • Transient, retry unchanged. TRANSIENT_ERROR (“Transient error, try again later”), PROCESSING_ERROR, PAYMENT_PROVIDER_ERROR, and arguably INSUFFICIENT_FUNDS, which is the only one of these where the passage of time fixes things, because payday is a real event.
  • Needs the customer. EXPIRED_CARD, EXPIRED_PAYMENT_METHOD, INVALID_PAYMENT_METHOD (“Payment method is invalid. Please update or create a new payment method.”), PAYMENT_METHOD_NOT_FOUND, INCORRECT_ADDRESS, INCORRECT_ZIP, CALL_ISSUER (“The card issuer requires the cardholder to call them”), AUTHENTICATION_REQUIRED and OFF_SESSION_REJECTED (“The off-session payment was rejected and requires customer action”).
  • Your problem. INSUFFICIENT_INVENTORY (“Not enough inventory found”), INVENTORY_ALLOCATIONS_NOT_FOUND (“No inventory location found or enabled”), PAYMENT_PROVIDER_IS_NOT_ENABLED, MERCHANT_ACCOUNT_ERROR, TEST_MODE (“Gateway is in test mode and attempted to bill a live payment method”) and PAYMENT_METHOD_NOT_SPECIFIED.

Why retrying the wrong class is worse than doing nothing

Retrying an EXPIRED_CARD four times produces four identical declines, four entries on the customer’s bank statement that may show as attempted transactions, and four opportunities for your dunning emails to say something that is not true. It also delays the only action that could work (asking the customer for a new card) by however long the retry schedule runs. The customer who would have updated their card on day one is, on day twelve, someone who has had three emails about a payment problem and has started thinking about whether they still want the subscription.

The merchant-side codes are worse, because a retry loop can send a dunning email for a failure the customer had nothing to do with. INSUFFICIENT_INVENTORY means you sold a renewal you could not fulfill; TEST_MODE means somebody left a gateway misconfigured. Neither should generate a message asking the customer to check their payment details, and both should page somebody internally. If your subscription platform cannot distinguish these, find that out before a support ticket does.

Card issuer behavior is the reason to be conservative about volume as well as targeting. RETRY_DECLINED (“The payment retry was declined”) exists as a distinct code, and DO_NOT_HONOR and FRAUD_SUSPECTED are decisions by the issuer rather than statements about the balance. Hammering those does not change the answer.

The merchant-side codes are worse, because a retry loop can send a dunning email for a failure the customer had nothing to do with.
Aashish Kasma & Bhavik Patel

The out-of-stock renewal

This is the edge case that loses customers quietly, because it does not look like a payment problem to anybody. A subscription renews, the item is out of stock, the billing attempt fails with INSUFFICIENT_INVENTORY, and (depending on how the recovery flow is written) the customer either gets a payment failure email about a card that is perfectly fine, or gets nothing at all and simply does not receive their delivery.

The fix is a decision, not code: for each subscription product, decide in advance whether a renewal may be billed and backordered, swapped for a substitute, or skipped with the cycle pushed forward. Then make sure the operational reality matches. INVENTORY_ALLOCATIONS_NOT_FOUND (“No inventory location found or enabled”) usually means a location was disabled or a product was never stocked at the location the subscription draws from, which is a data problem that will recur every cycle until somebody fixes the source.

This is also where the subscription-vs-catalog conflict shows up. If your subscription items share stock with one-off sales, a good week of one-off trading eats the inventory your renewals were counting on. Reserving stock for subscriptions is unglamorous and it is the difference between predictable recurring revenue and a monthly scramble.

Authentication, and the renewal nobody is present for

Subscription renewals are off-session by definition. The customer is not at their keyboard. When an issuer decides it wants the cardholder, the attempt cannot simply succeed. Shopify has a dedicated webhook topic for this: SUBSCRIPTION_BILLING_ATTEMPTS_CHALLENGED, documented as occurring “when the financial instutition challenges the subscripttion billing attempt charge as per 3D Secure” (the typos are Shopify’s). There are matching error codes: AUTHENTICATION_REQUIRED (“Additional authentication is required to complete the payment”), EXPIRED_BUYER_ACTION (“The required buyer action has expired”) and OFF_SESSION_REJECTED.

The practical consequence is that a challenged renewal has a deadline. The customer has to act, and the action expires. A recovery flow that treats a challenge like a decline (queue it, retry in three days) will hit EXPIRED_BUYER_ACTION and start again. Challenges need to go to the front of the queue with a message that says what the customer must do, not a generic “your payment failed”.

Do not charge anyone twice

Every recovery system retries, and every retry is an opportunity to bill someone twice, for example when a request times out on your side after Shopify has accepted it. subscriptionBillingAttemptCreate takes an idempotencyKey for exactly this, ensuring the billing attempt executes only once so that a retried request cannot produce a duplicate charge. Generate the key from the contract and the billing cycle, not from a random value per attempt, or you have an idempotency key that makes every retry unique and protects nothing.

The mutation is also asynchronous: the payload returns a subscriptionBillingAttempt with a ready field that is false initially, and processing continues after the mutation returns. So the response is not the outcome. The outcome arrives on the SUBSCRIPTION_BILLING_ATTEMPTS_SUCCESS and SUBSCRIPTION_BILLING_ATTEMPTS_FAILURE webhook topics, both of which Shopify documents as requiring the read_own_subscription_contracts scope. Anything that reads the mutation response as a result (a dashboard, a support tool, a log line someone will trust at 2am) is reading it wrong.

What we would build, and what we would not

If you run subscriptions on a platform, most of this already exists and the work is configuration and honesty: find out which error codes it branches on, what it emails for each, and whether merchant-side failures reach a human. Rebuilding a dunning engine that your platform already ships is the expensive way to discover it was adequate.

Build custom when the branching your business needs is not expressible in the platform: inventory-aware renewals, B2B terms where a failed charge should not suspend an account, or per-market retry rules. And measure the split before you invest in any of it. Everyone quotes a figure for how much subscription churn is involuntary; we have not found one with a methodology worth repeating, and your own failure codes over three months are a better number than anybody’s benchmark.

Questions this raises

How do you handle a failed subscription payment on Shopify?

Branch on the error code. `SubscriptionBillingAttemptErrorCode` reports more than sixty reasons: retry the transient ones such as `TRANSIENT_ERROR` and `PROCESSING_ERROR`, ask the customer to act on `EXPIRED_CARD`, `INVALID_PAYMENT_METHOD` and `AUTHENTICATION_REQUIRED`, and route merchant-side ones such as `INSUFFICIENT_INVENTORY` or `PAYMENT_PROVIDER_IS_NOT_ENABLED` to your own team rather than to the customer.

How many times should you retry a failed renewal?

It depends entirely on the code, which is why a single global answer is the wrong shape. Codes that cannot change without the customer doing something should not be retried at all beyond one attempt. For transient failures, retry, but note that Shopify has a distinct `RETRY_DECLINED` code, so the issuer is capable of treating your retries as their own signal.

Why does a subscription fail to bill when the customer’s card is fine?

Usually inventory. `INSUFFICIENT_INVENTORY` (“Not enough inventory found”) and `INVENTORY_ALLOCATIONS_NOT_FOUND` (“No inventory location found or enabled”) are both billing attempt failures with no payment cause. Other non-payment causes include `PAYMENT_PROVIDER_IS_NOT_ENABLED`, `MERCHANT_ACCOUNT_ERROR` and `TEST_MODE`.

What is a challenged billing attempt?

A renewal the customer’s bank wants authenticated under 3D Secure. Shopify emits it on the `SUBSCRIPTION_BILLING_ATTEMPTS_CHALLENGED` webhook topic, separate from failure, and the related codes include `AUTHENTICATION_REQUIRED` and `EXPIRED_BUYER_ACTION`. Because the required buyer action can expire, challenges need prompt, specific messaging rather than a place in the normal retry queue.

Can you send your own payment update email instead of Shopify’s?

You can, but consider whether you need to. `customerPaymentMethodSendUpdateEmail` sends a secure link to update a vaulted payment method and accepts only `from` and `bcc` as customization. A bespoke alternative means owning a credential-handling page and a secure expiring link. Unless the branded template is worth that, use the mutation and put the effort into the timing.

How do you avoid double-charging during recovery?

Pass an `idempotencyKey` to `subscriptionBillingAttemptCreate`, derived from the contract and the billing cycle rather than generated fresh per request. And treat the mutation as asynchronous. The returned attempt has a `ready` field that starts false, so the real outcome arrives on the success and failure webhooks, not in the response.

General information, not legal, tax or financial advice. Rates, limits and deadlines come from the vendors and authorities named, and they change: check the linked source before relying on one.

PART OF OUR SERVICE

Wire Shopify into the systems your business already runs on

ERP, subscriptions, custom apps and configurators

EXPLORE INTEGRATE →

NEXT STEP

Free store audit

A senior Shopify engineer reviews your storefront, theme performance and checkout, then sends a prioritized list of fixes.

A hand gestures towards a small shop doorway tied with colorful ribbons, with a tablet on a workbench inside