Guide

How recurring payments actually work on Shopify

The four-actor chain — customer, Shopify checkout, your subscription app, and the payment gateway — is simpler than most merchants assume, but each handoff has a failure mode worth understanding. This guide walks through what happens between sign-up and the second renewal, why some charges fail, and what Shopify owns vs. what your app owns.

16 min readUpdated 21 May 2026By SimpleSubscription Team
On this page (11)
  1. The four actors in every recurring charge
  2. Signup: vaulting the payment method
  3. What Shopify stores after signup
  4. The renewal trigger: your app's cron and SubscriptionBillingAttempt
  5. What Shopify does inside a billing attempt
  6. Why recurring payments fail (and what Shopify returns)
  7. Idempotency, retries, and avoiding double-charges
  8. Currency, tax, and address on the renewal
  9. Why you don't (and can't) bill via Stripe directly
  10. Refunds and chargebacks on recurring orders
  11. Webhooks: how your app stays in sync

Most subscription merchants treat recurring payments as a black box: a customer subscribes, money shows up monthly, and when something breaks support handles it. That works until something does break — a card stops billing, a refund needs to clawback only the renewal portion, a dispute lands and the processor wants evidence the charge was authorised, or a customer asks why the renewal amount changed when they updated their address. Suddenly the black box has to be opened. This guide unpacks what happens at each step of a Shopify recurring charge: which actor (customer, Shopify checkout, your subscription app, Shopify Payments) is responsible for what, where state is stored, what the renewal trigger actually is, and why the answer to 'why did this charge fail?' is almost always one of about six known reasons. None of this requires a developer to follow — but understanding it makes you a much better operator of a subscription business and saves a lot of support tickets that are really just confusion about who owns the data.

The four actors in every recurring charge

Every Shopify subscription charge involves the same four actors handing state to each other in a fixed order. Once you can name them, the rest of the system becomes legible.

  1. The customer — provides the card and the shipping address at signup, and can update either via your customer portal later. Owns nothing about the schedule.
  2. Shopify checkout — runs the initial sign-up transaction, vaults the card with the payment gateway, and creates a SubscriptionContract stored on Shopify's side. After this it doesn't do anything until your app pokes it.
  3. Your subscription app — owns the schedule. Runs a cron job that checks which contracts are due today and tells Shopify, 'please attempt to bill these.' It does not handle money or card data directly.
  4. Shopify Payments (or the underlying gateway) — receives the bill request, attempts the charge against the vaulted payment method, returns success or a failure code. On success Shopify creates the renewal order and triggers fulfillment.

Notice what your subscription app does NOT do: touch the card, hold the customer's PAN, run charges directly, decide whether to retry on a hard decline. All of that lives inside Shopify and the gateway. Your app is essentially a scheduler that fires a 'try this contract now' message at the right moment. This is also why migrating between subscription apps is mostly a contract-handover problem and almost never a payments problem — the cards themselves never leave Shopify.

Customer signs up, Shopify holds the contract and card, your app schedules the trigger, Shopify Payments charges. Each actor has exactly one job.

Signup: vaulting the payment method

When a customer completes a subscription checkout for the first time, Shopify Payments does two things: charges the first order normally, and stores the card with the payment processor (Stripe Connect under the hood for Shopify Payments merchants) so it can be charged again later without the customer present. The card data never lives on Shopify's servers — what Shopify stores is a payment method ID, an opaque token that says 'gateway X has card Y on file, here's the handle to charge it.'

This is PCI-compliance plumbing. Your app does not see card numbers, expirations, or CVVs. You'll only ever see the last four digits, the brand (Visa, MC, Amex), and an internal identifier. The actual vaulting happens inside the iframe-rendered card field Shopify renders at checkout — even Shopify's own theme code can't read the PAN. This matters because it means recurring payments on Shopify only work with gateways that support stored payment methods, which in practice means Shopify Payments (most merchants), PayPal, Shop Pay, and a handful of region-specific options. Most third-party gateways still don't support card vaulting through Shopify, and merchants who try to launch subscriptions on one of those gateways are surprised when the first renewal can't fire because there's no stored payment method on file.

Tip
Check gateway support before you launch

Shopify's docs list which gateways support subscription billing. As of 2026 that's effectively Shopify Payments in most markets, plus a small set of regional gateways with explicit subscription certification. If you're on Adyen, Authorize.Net, or a niche gateway and want to run subscriptions, verify in the Shopify admin under Settings → Payments that your gateway shows the 'recurring payments supported' indicator before installing any subscription app.

The card lives at the gateway as a tokenised payment method. Shopify and your app only ever see the handle, never the PAN.

What Shopify stores after signup

After the first checkout completes, Shopify writes a SubscriptionContract record into its own database. This is the canonical source of truth for the schedule. It contains the customer ID, the payment method handle, the shipping address, the billing policy (how often to charge), the delivery policy (how often to ship), the pricing policy (any discount applied to each renewal), and a list of subscription lines (which products, which selling plan, quantity, price). It does NOT contain payment processor secrets, card numbers, or any data your app generates.

Your subscription app reads this contract when it needs to know what to bill and write to it when the merchant or customer makes a change (skip next order, update address, swap product, change cadence). Shopify's Subscription Contract API is the GraphQL surface that exposes all of this. Importantly, when you 'update' a contract you usually create a SubscriptionDraft first — an in-progress copy you can mutate freely — then commit it to replace the live contract atomically. This prevents half-edited contracts from being billed against in a race condition.

Watch out
Don't try to maintain your own schedule database

Some apps keep a parallel copy of every subscription's next-billing-date in their own Postgres because querying Shopify is slower. That parallel copy drifts. Customer skips a delivery in the portal, your app updates Shopify, Shopify pushes nextBillingDate forward, your local cache still has the old date, the cron fires anyway, and you double-bill. Read the date off Shopify on every cron tick or subscribe to subscription_contracts/update and invalidate aggressively.

Shopify owns the contract. Your app reads from it and writes to it via the API — never holds a parallel copy of the truth.

The renewal trigger: your app's cron and SubscriptionBillingAttempt

Here's the part most merchants are surprised by: Shopify does not automatically run subscription renewals. Shopify holds the contract and the card, but it doesn't have a built-in scheduler that says 'today is Tuesday, fire all contracts due Tuesday.' That scheduling job is your subscription app's responsibility. Most apps run a cron — typically every 15 minutes — that queries Shopify for contracts with nextBillingDate on or before now, then fires a SubscriptionBillingAttempt mutation for each one.

A billing attempt is a request to Shopify saying, 'please try to charge this contract right now.' Shopify then asks the gateway to authorise and capture the stored payment method, and if it succeeds, creates a real Shopify order against the contract's products and customer, with the contract's address and tax jurisdiction applied at the moment of charge. The order then enters Shopify's normal fulfillment pipeline — exactly like a one-time purchase order — and your fulfillment apps, shipping rules, and webhooks all see it as a regular order with a special tag indicating it was billed from a subscription.

# Trigger a renewal: SubscriptionBillingAttemptCreate mutation
mutation BillContract($contractId: ID!, $idempotencyKey: String!) {
  subscriptionBillingAttemptCreate(
    subscriptionContractId: $contractId,
    subscriptionBillingAttemptInput: {
      idempotencyKey: $idempotencyKey
    }
  ) {
    subscriptionBillingAttempt {
      id
      ready
      errorCode
      errorMessage
      order { id }
    }
    userErrors { field message }
  }
}

The idempotencyKey is critical — pass the same value if you ever retry the same attempt and Shopify will refuse to double-charge. Most apps generate this key from the contract ID plus the billing date, so a retry of the same scheduled billing always collapses into the same attempt server-side.

Your app fires the renewal; Shopify executes it. Always include an idempotency key keyed to contract + date.

What Shopify does inside a billing attempt

When the billing attempt fires, Shopify runs a small pipeline before any money moves. First, it re-resolves the contract: re-reads the lines, the address, the discount policy, the customer's currency. This means an attempt always charges based on the contract's current state at the moment of the attempt, not the state when the cron decided it was due (a tiny window, but worth knowing about if a customer makes an edit in those last few seconds). Second, it calculates tax based on the current shipping address against Shopify Tax rules. Third, it asks the gateway to authorise and capture the stored payment method for the calculated total.

If the authorisation succeeds, Shopify creates the renewal order, marks the attempt successful, fires the subscription_billing_attempts/success webhook, and the order enters the normal fulfillment flow. If the authorisation fails (declined card, expired card, insufficient funds, fraud flag), Shopify marks the attempt failed with an error code, fires the subscription_billing_attempts/failure webhook, and your app is responsible for deciding whether to retry, send a dunning email, pause the subscription, or escalate. Shopify will not retry on its own — retry policy is entirely your app's responsibility.

Payment Recovery
67%Auto-recovered
3.1xMore retries vs avg
Day 0
Payment failed
Day 3
Smart retry #1
Retry at optimal time. Card updater checked.
Day 7
Smart retry #2
Day 14
Final notice
EmailSMSCard updaterChurn prediction
The renewal timeline: cron tick → billing attempt → success creates an order, failure enters dunning
Shopify re-resolves the contract, calculates tax, asks the gateway. Success creates an order. Failure is handed back to your app to retry.

Why recurring payments fail (and what Shopify returns)

About 5-10% of subscription renewals fail on the first attempt — that's the industry baseline across consumer-goods subscriptions on Shopify. Knowing the failure reasons matters because the right retry behaviour is different for each. A hard decline ('this card is closed') should not be retried 5 times in 48 hours; an issuer soft-decline ('insufficient funds') often clears within a day or two.

  • EXPIRED_PAYMENT_METHOD — the card is past its expiration date. No amount of retrying helps. Send a 'please update your card' email immediately.
  • INSUFFICIENT_FUNDS — soft decline. Most apps retry 24h later, then 72h later, then 7 days, before giving up.
  • AUTHENTICATION_ERROR — typically Strong Customer Authentication (SCA) required in the EU/UK. The renewal needs a customer-present step. Send a payment-method-update link.
  • CARD_NUMBER_INVALID / CARD_DECLINED — issuer refused. Could be a fraud flag, could be a closed account. Retry once, then escalate to email.
  • FRAUD_SUSPECTED — the gateway flagged the transaction. Stop attempting; ask the customer to confirm.
  • PAYMENT_METHOD_NOT_FOUND — the customer deleted the payment method on Shopify but you're still attempting to bill it. Pause the contract and email.

Shopify exposes the failure reason via the errorCode field on the billing attempt and via the subscription_billing_attempts/failure webhook payload. A well-built subscription app reads this code and branches its dunning logic accordingly — generic 'we couldn't charge your card' emails are far less effective than 'your card expired, click here to update' when you actually know which one happened.

Failures are categorical, not random. Match retry cadence and dunning copy to the specific error code Shopify returns.

Idempotency, retries, and avoiding double-charges

The scariest subscription bug is the one that bills a customer twice for the same month. The cleanest way to prevent it is to lean on Shopify's idempotency guarantee: every billing attempt accepts an idempotencyKey, and if you submit the same key twice, Shopify returns the original attempt instead of creating a new one. Your app should generate this key deterministically from data that's stable for a given renewal — typically the contract ID plus the scheduled billing date in ISO format.

This protects you from the classic distributed-systems failure where your cron times out, your worker crashes after firing the API call but before recording success, and the retry fires the same renewal again. With a stable idempotency key, the retry collapses into the original attempt server-side. Without one, you double-charge.

Watch out
Never generate idempotency keys with timestamps

Using Date.now() or a random UUID as your idempotency key defeats the whole point — every retry generates a new key, so Shopify sees a fresh request every time and bills again. The key must be derivable from the same input data on every retry. contract:{id}:bill:{YYYY-MM-DD} is a reasonable pattern.

For retries after a soft failure (insufficient funds, transient gateway error), generate a NEW idempotency key — this is a deliberately different attempt, separated by hours or days, and you do want a fresh charge attempt. The contract+date pattern works because soft retries usually happen on different dates by then; if you're retrying same-day, add a retry sequence number to the key.

Idempotency keys are required, not optional. Make them deterministic per renewal and your app cannot double-charge by accident.

Currency, tax, and address on the renewal

A subscription contract stores the customer's currency, billing address, and shipping address at signup — but all three can change between renewals, and the renewal charges what's current at the moment of the attempt, not what was signed up for. This is mostly the behaviour you want, but it has a few edge cases worth knowing about.

Currency is fixed at signup for a given contract. If you've enabled Shopify Markets and a customer subscribed at €30/month, the renewals charge €30/month forever — even if the customer moves to a USD-currency country. To switch currency you'd cancel the contract and re-subscribe. This is a Shopify constraint, not an app limitation.

Tax is recalculated at every renewal based on the current shipping address. A customer who signs up in tax-free Oregon and moves to California is taxed at California rates from the next renewal forward. Your app needs to push address changes to Shopify promptly — most native-integration apps subscribe to portal address updates and call subscriptionContractUpdate within seconds, but some legacy apps batch updates and create a window where the cron fires before the address update has propagated. Verify your app's behaviour here if you operate across tax jurisdictions.

Shipping address updates in the customer portal flow through to the next renewal automatically. Billing address (the address Shopify Payments verifies the card against) is tied to the payment method, not the contract — updating it requires the customer to update their stored card, which most portals expose as a 'Update payment method' link that opens Shopify's hosted card-update flow.

Currency is sticky. Tax is recalculated every renewal. Address updates need to land in Shopify before the cron fires.

Why you don't (and can't) bill via Stripe directly

Developers familiar with Stripe Subscriptions sometimes ask: why doesn't my Shopify subscription app just create a Stripe subscription and run it through Stripe directly? The short answer: because Shopify Payments IS Stripe under the hood (Stripe Connect), but the card vault is owned by Shopify, not by you, so you can't see or move the card to a separate Stripe account. The card is stored in Shopify's payment-method namespace; your app doesn't have access to the underlying Stripe customer ID.

This is by design. Shopify owns the gateway relationship for Shopify Payments merchants because they need to enforce platform policy (fraud, disputes, payouts, refunds), and exposing the underlying Stripe primitives would let apps bypass that. It also means subscription apps don't have to be PCI-compliant themselves — none of them touch the card. The trade-off is you cannot do things Stripe Subscriptions does natively (like SmartRetries with machine-learning retry timing) because you're going through Shopify's billing pipeline, which has its own retry semantics and webhooks.

For merchants on a non-Shopify-Payments gateway (PayPal, Adyen in supported markets), the same principle applies: the gateway owns the vaulted card, Shopify owns the customer/contract relationship, your app schedules the trigger. No matter which gateway you use, the architecture is the same — your app is never the entity moving money.

Shopify is the merchant of record for subscription charges. Your app is a scheduler. Direct Stripe is not an option on the Shopify platform.

Refunds and chargebacks on recurring orders

A renewal order, once created, behaves exactly like a normal Shopify order for refund and dispute purposes. You refund through the standard Shopify admin (or via the refundCreate mutation), and the refund goes back to the same card the renewal was billed on. Partial refunds work the same way — refund only the shipping, only one line, or a custom amount.

Refunding a renewal does NOT cancel the underlying subscription contract. If you want to refund the renewal and stop future charges, you have to do both: issue the refund AND call subscriptionContractCancel to halt the contract. Forgetting the second step is a common cause of 'you refunded me but charged me again next month' support tickets.

Chargebacks are where the contract model really earns its keep. When a chargeback lands, Shopify Payments asks for evidence the charge was authorised and the customer agreed to recurring billing. The subscription contract — with its signup-date timestamp, the customer's accepted terms, the original order, and the cancel-policy URL — is what you submit as evidence. Stores that document their auto-renewal disclosure clearly in the signup flow and the order-confirmation email win the majority of subscription chargebacks. Stores that don't, lose them.

Checklist
Build your subscription chargeback evidence pack now
  • Screenshot of the product page widget showing recurring price + frequency at the moment of signup
  • Order confirmation email template with renewal price + cancel instructions visible
  • Link to the public terms of service / subscription policy URL referenced at checkout
  • The customer's portal access URL (proving self-service cancel was available)
  • Renewal notification email logs (if you send pre-renewal reminders, which the EU and several US states require)
  • Payment method history showing the same card was on file from signup through the disputed charge
Refunds work like any Shopify order. Chargebacks are won by the merchant who can produce a clean disclosure trail.

Webhooks: how your app stays in sync

Shopify pushes a webhook every time meaningful state changes in the subscription lifecycle. Your app subscribes to these and reacts — most often, by updating internal analytics, sending dunning emails, or invalidating cached data. The set of webhooks worth subscribing to is small and well-defined.

  • subscription_contracts/create — fires when a new contract is created (first checkout, or via API)
  • subscription_contracts/update — fires when any field changes (address, lines, status, next billing date)
  • subscription_billing_attempts/success — a renewal charge succeeded; an order was created
  • subscription_billing_attempts/failure — a renewal charge failed; the payload includes the errorCode for routing dunning
  • orders/create — fires for the renewal order itself (use this for fulfillment-side reactions like 3PL handoff)
  • customers/update — useful for keeping internal customer records in sync, especially payment method changes

Two operational notes. Webhooks can arrive out of order, especially under load — design your handlers to be idempotent and to tolerate seeing 'update' before 'create' (look up by ID, upsert). And Shopify retries failed webhooks for up to 48 hours with exponential backoff; if your endpoint returns a 5xx, expect to see the same event again. Always return 2xx fast (under 5 seconds) and do real processing in a background queue.

Six webhooks cover the whole subscription lifecycle. Handle them idempotently and return 2xx fast — process the work async.

Recurring payment questions

Does Shopify support recurring payments natively?

Yes — Shopify has supported subscription contracts natively since 2021, via the Subscription Contract API. The platform stores the contract, vaults the payment method, and executes billing attempts. What Shopify does NOT include natively is the merchant-facing UI (selling plan management, customer portal, cancel flow, dunning emails). Those come from a subscription app installed on top.

Do I need Stripe to run subscriptions on Shopify?

No — and in fact you can't bill via Stripe directly for Shopify subscription orders. Shopify Payments uses Stripe Connect under the hood, but the card vault is owned by Shopify; your subscription app never sees the underlying Stripe customer. Billing flows through Shopify's pipeline, not a separate Stripe account.

Which payment gateways support recurring payments on Shopify?

Shopify Payments is the primary supported gateway in most markets. PayPal Express and Shop Pay also work for subscription billing. Most third-party gateways (Authorize.Net, Worldpay, regional gateways) don't support card vaulting through Shopify, so renewals fail because no stored payment method is available. Check the gateway's capability list in your Shopify admin's Settings → Payments page before installing a subscription app.

How does Shopify handle automatic card updater?

Shopify Payments participates in the Visa and Mastercard Account Updater services automatically. When a customer's card is reissued (new expiry, new number after a lost-card replacement), the network pushes the new credentials to Shopify, which updates the stored payment method without any customer action. This silently recovers a meaningful fraction of renewals that would otherwise have failed with EXPIRED_PAYMENT_METHOD.

What currency is a renewal charged in?

Whatever currency the contract was signed up in. Currency is fixed per contract for the life of the subscription — even if the customer moves countries or your store enables additional currencies via Shopify Markets. Switching a subscriber to a different currency requires cancelling the contract and re-subscribing.

What's the difference between a billing attempt and an order?

A billing attempt is the request to charge — it can succeed or fail. An order is what Shopify creates when the billing attempt succeeds. Every successful billing attempt produces exactly one order. Failed attempts don't produce orders; they show up in the subscription contract's billing attempt history with an error code.

How often does Shopify retry a failed billing attempt?

Shopify does not retry on its own. Retry scheduling is your subscription app's responsibility — when a billing attempt fails, the <code>subscription_billing_attempts/failure</code> webhook fires and your app decides whether to retry, when, how many times, and whether to escalate to dunning emails. A common pattern is 1 day, 3 days, 7 days, then pause + escalation.

Can I change the price of an existing subscription?

Yes, via the Subscription Contract API. Most subscription apps expose this as a 'change product' or 'edit line' flow. Under the hood it creates a SubscriptionDraft, applies the price change, and commits. The new price takes effect on the next renewal. Be aware: depending on jurisdiction, you may be legally required to notify the customer in advance of a price change (the EU's modernised Consumer Rights Directive requires this; California's auto-renewal law mandates a 30-day notice for material changes).

How do refunds work on a subscription renewal?

Exactly like any Shopify order. Refund through the admin or via the refundCreate mutation; the refund goes back to the same card. Refunding the order does NOT cancel the underlying contract — if you want to stop future charges too, you must also call subscriptionContractCancel. Forgetting the second step is the most common cause of 'you refunded me but billed me again' tickets.

What happens if a customer disputes a recurring charge?

Shopify Payments will ask for evidence the charge was authorised. The subscription contract — with its signup timestamp, the linked customer, the original order, and the disclosed cancellation policy — is what you submit. Stores with clear auto-renewal disclosure at signup and in the confirmation email win the majority of these disputes. Stores without it usually lose.

Can I trigger a billing attempt manually?

Yes — both via the Shopify admin (in some apps) and via the subscriptionBillingAttemptCreate mutation directly. The most common reason to do this is a 'charge now' button for a customer who wants to receive their next box early. Always include an idempotency key so an accidental double-click doesn't double-charge.

Does Shopify handle Strong Customer Authentication (SCA) for renewals?

Recurring charges in the EU/UK are exempt from SCA in most cases under the 'merchant-initiated transaction' exemption, provided the original signup was SCA-authenticated. If the issuer challenges a renewal anyway (which happens occasionally), Shopify returns AUTHENTICATION_ERROR and your app should email the customer a link to complete a customer-present payment method update. Most renewals never trigger this.

The pillar

Read the complete Shopify Subscription App overview

Pricing, every feature, side-by-side comparison, FAQ — the single page that ties all these guides together.

Go to the pillar

Run your recurring billing on infrastructure you understand

SimpleSubscription handles the cron, idempotency, dunning, and webhook handling on top of Shopify's native billing — flat fee, zero transaction percentage.

Install on Shopify

Start free · 14-day trial on paid plans · Zero transaction fees · Free migration