Payment method update dunning creates new billing cycle instead of retrying overdue unbilled cycle

We’ve built our own subscription app using Shopify’s Subscriptions API, and we’re seeing unexpected behaviour when a customer updates their payment method on a paused subscription contract.

Expected behaviour: When a customer updates their payment method, Shopify’s dunning retries the original overdue billing cycle (e.g., one that failed on April 23), marks it as BILLED, and resumes the contract.

Actual behaviour: Shopify’s dunning creates a billing attempt via subscriptionBillingAttemptCreate, which starts a new billing cycle from today’s date (e.g., April 30) rather than targeting the existing UNBILLED cycle. The original April 23 cycle remains UNBILLED + HAS_ATTEMPT indefinitely.

Evidence from our contract history:

  • April 23: Billing attempt created → FAILED
  • April 24–26: Further retries → all FAILED → contract PAUSED
  • April 30 19:34: Customer updates payment method
  • April 30 19:36: Billing attempt SUCCEEDS (triggered by Shopify’s native dunning) → contract ACTIVE, billing date advanced to May 30 (i.e., a new 30-day cycle was created from April 30)
  • April 23 billing cycle: still shows as UNBILLED in the billing cycles list

Because the April 23 cycle is still UNBILLED + HAS_ATTEMPT, our retry jobs (using subscriptionBillingCycleBulkCharge) pick it up days later and charge the customer again — a double charge.

Questions:

  1. Is this intentional behaviour — that dunning on payment method update always starts a fresh cycle from today rather than retrying the overdue cycle?
  2. Is there a way to configure dunning to retry the specific UNBILLED cycle rather than creating a new one?

We’re on API version 2026-01. Happy to provide more details if helpful.

Hey @AubV60, typically, the billing attempt orchestration and duplicate-charge prevention is handled on the app side. Model a subscriptions solution

To safeguard against the double charge you’re seeing you should use the idempotencyKey on subscriptionBillingAttemptCreate. If your bulk-charge job reuses the same key for a given cycle, a second attempt on a cycle that already succeeded won’t charge again.

Alongside that, your retry job can read SubscriptionBillingCycle.status and billingAttempts before charging, and pass billingCycleSelector to target a specific cycle (e.g. the April 23 one) instead of defaulting to the current one.