Hi there,
Our subscription app makes use of the subscriptionBillingCycleBulkCharge graphQL API to charge the subscriptions for the current date using a daily cron job.
The app calls the graphql endpoint with the following filters:
{
contractStatus: ['ACTIVE'],
billingCycleStatus: 'UNBILLED',
}
It has been running perfectly for a long time, but on Nov 28th, we noticed that the subscriptionBillingCycleBulkCharge charged both BILLED and UNBILLED cycles.
This resulted in the double billing of for 50+ subscriptions.
Here is the result json that was returned from the subscriptionBillingCycleBulkResults of the job (JobID: gid://shopify/Job/2503ebf0-27ae-4120-b17c-43d62c06ec3d)
Weâve checked our logs and we definitely sent the filters to the graphql endpoint and our code hasnât changed. All other recent jobs also had no problems.
Just for context, those 50+ subscription contracts were manually billed using the subscriptionBillingAttemptCreate a few days prior on 25th Nov.
1 Like
Hey @tai_almond, I want to make sure I understand what youâre seeing. The status field in subscriptionBillingCycleBulkResults shows the current status of the billing cycle, so a BILLED in the response doesnât necessarily mean the filters were ignored.
To confirm double-billing occurred, could you query one of the affected billing cycles and share the billingAttempts? Something like:
query {
subscriptionBillingCycle(
contractId: "gid://shopify/SubscriptionContract/XXX"
billingCycleIndex: 6
) {
cycleIndex
status
billingAttempts(first: 10) {
edges {
node {
id
createdAt
ready
order { id }
}
}
}
}
}
If there are two successful billing attempts with orders on the same cycle, that would confirm the double-billing.
Hi @KyleG-Shopify ,
Thanks for the swift response.
Iâve run the billingAttempts query on the affected cycle (cycle 3) and it only shows 1 billed attempt for the cycle.
However, when i run the the `billingAttempts` query, I see both the attempts:
query {
subscriptionContract(id: "gid://shopify/SubscriptionContract/22893428922") {
id
billingAttempts(first: 100, reverse: true) {
nodes {
id
createdAt
ready
processingError { message }
order { id name }
}
}
}
}
For the âOrder nowâ function in my app (which lets customers order a subscription) Iâm currently using the subscriptionBillingAttemptCreate mutation. Does this mean that I should be using the subscriptionBillingCycleCharge mutation instead?
I thought both the mutations worked the same.
Hey @tai_almond, thanks for running those queries. This helps clarify whatâs happening.
Thereâs some nuance worth unpacking here, since your setup involves two different charging mechanisms that need to work together.
For context, each billing cycle has a cycleIndex (sequential: 1, 2, 3âŚ), date boundaries (cycleStartAt/cycleEndAt), a billingAttemptExpectedDate, and a status (UNBILLED or BILLED). The About subscription billing cycles guide has a helpful diagram showing how these fit together.
When you use subscriptionBillingAttemptCreate for manual charges, the billingCycleSelector is optional. If youâre not explicitly targeting a cycle you may be unexpectadly charging the wrong cycle.
Since your subscriptionBillingCycleBulkCharge uses billingAttemptExpectedDateRange to find cycles where the billingAttemptExpectedDate falls within your specified range. What may be happening here is this bulk charge is picking up a different cycle than you charged earlier. Consider adding billingAttemptStatus: NO_ATTEMPT to your bulk charge filters as an additional safety net for a charge that may not have a correct status at the time of the mutation.
For your âOrder nowâ feature, the Manage billing cycle contracts guide recommends using subscriptionBillingCycleCharge instead. This mutation requires an explicit billingCycleSelector (by index or date), which directly links the billing attempt to that specific cycle and updates its status to BILLED.
Hey Tai, did the above help at all?