[CRITICAL BUG] subscriptionBilling CycleBulkCharge filter not respected

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.