Skip to main content

Billing Cycles

Billing cycles represent individual billing periods within a subscription contract. Each cycle tracks when a billing attempt should occur, whether it was successful, and the resulting order. Understanding billing cycles is essential for managing subscription payments and deliveries.

What is a Billing Cycle?

A billing cycle is a specific occurrence of a scheduled billing attempt for a subscription contract. Each cycle contains:
  • A cycle index (starting at 0 for the first cycle)
  • The expected billing attempt date
  • Billing attempt results and any associated orders
  • Skip status (if the customer skipped this cycle)
  • Current billing cycle status
Billing cycles are automatically created by Shopify based on the subscription contract’s billing policy.

Querying Billing Cycles

The app uses GraphQL to fetch billing cycle information:
query SubscriptionBillingCycles(
  $contractId: ID!
  $first: Int!
  $startDate: DateTime!
  $endDate: DateTime!
) {
  subscriptionBillingCycles(
    contractId: $contractId,
    first: $first,
    billingCyclesDateRangeSelector: { 
      startDate: $startDate, 
      endDate: $endDate 
    }
  ) {
    edges {
      node {
        billingAttemptExpectedDate
        cycleIndex
        skipped
        billingAttempts(first: 10) {
          edges {
            node {
              id
              order {
                id
                createdAt
              }
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
    }
  }
}

Billing Schedules

The reference app uses a BillingSchedule model to control when billing cycles are processed for each shop:
model BillingSchedule {
  id        Int      @id @default(autoincrement())
  shop      String   @unique
  hour      Int      @default(10)
  timezone  String   @default("America/Toronto")
  active    Boolean  @default(true)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Schedule Configuration

  • shop: The Shopify shop domain
  • hour: The hour of day (0-23) when billing should occur
  • timezone: The timezone for the scheduled hour
  • active: Whether billing is enabled for this shop
  • createdAt/updatedAt: Timestamps for tracking changes
import type {BillingSchedule, Prisma} from '@prisma/client';
import {Factory} from 'fishery';
import prisma from '~/db.server';

export const billingSchedule = Factory.define<
  Prisma.BillingScheduleCreateInput,
  {},
  BillingSchedule
>(({onCreate, sequence}) => {
  onCreate((data) => prisma.billingSchedule.create({data}));

  return {
    shop: `shop-${sequence}.myshopify.com`,
  };
});

Billing Attempt Process

The app processes billing cycles through a series of background jobs:

1. Recurring Billing Job

The main job that kicks off the billing process:
import {DateTime} from 'luxon';
import {Job} from '~/lib/jobs';

export class RecurringBillingChargeJob extends Job<{}> {
  async perform(): Promise<void> {
    const targetDate = DateTime.utc().startOf('hour').toISO() as string;

    const params: Jobs.ScheduleShopsForBillingChargeParameters = {targetDate};

    this.logger.info(
      {params},
      `Scheduling ScheduleShopsToChargeBillingCyclesJob to run at ${targetDate}`,
    );

    const job = new ScheduleShopsToChargeBillingCyclesJob(params);
    await jobs.enqueue(job);
  }
}
Billing jobs run at the start of each hour and process all shops scheduled for that time.

2. Job Execution Flow

RecurringBillingChargeJob identifies the current hour and queues shop-specific billing jobs.

Billing Cycle Status

Billing cycles can be in different states:
const BILLING_CYCLE_BILLED_STATUS = 'BILLED';
  • UNBILLED: Cycle is scheduled but hasn’t been processed yet
  • BILLED: Billing attempt was successful and an order was created
  • SKIPPED: Customer or merchant skipped this billing cycle

Skipping Billing Cycles

Customers can skip upcoming billing cycles:
mutation SkipBillingCycle($billingCycleInput: SubscriptionBillingCycleInput!) {
  subscriptionBillingCycleSkip(billingCycleInput: $billingCycleInput) {
    billingCycle {
      cycleIndex
      skipped
    }
    userErrors {
      field
      message
    }
  }
}
Skipped cycles don’t generate orders or billing attempts. Make sure customers understand they won’t receive products for skipped cycles.

Querying Subscription with Billing Cycle

The app provides a helper to find contracts with specific billing cycle data:
const {subscriptionContract, subscriptionBillingCycle} =
  await findSubscriptionContractWithBillingCycle({
    shop: shopDomain,
    contractId,
    date,
  });
This query returns both the contract and the billing cycle for the specified date, which is useful for dunning management and billing attempt handling.

Billing Attempts

Each billing cycle can have multiple billing attempts if payments fail:
export interface BillingAttempt {
  id: string;
  errorCode?: string | null;
  processingError?: BillingAttemptProcessingError | null;
}

export interface BillingAttemptProcessingError {
  code: string;
  insufficientStockProductVariants?: InsufficientStockProductVariant[];
}

Attempt Tracking

The billing cycle tracks all attempts:
billingAttempts: {
  edges: [
    {
      node: {
        id: string;
        ready: boolean;
        order?: {
          id: string;
          createdAt: string;
        };
      };
    }
  ];
};
The ready field indicates whether Shopify has finished processing the billing attempt. Always check this before taking action on attempt results.

Past Billing Cycles

The app provides queries to fetch historical billing cycles:
query SubscriptionPastBillingCycles(
  $contractId: ID!
  $first: Int!
) {
  subscriptionBillingCycles(
    contractId: $contractId,
    first: $first,
    billingCyclesIndexRangeSelector: { 
      startIndex: 0 
    }
  ) {
    edges {
      node {
        cycleIndex
        billingAttemptExpectedDate
        status
        billingAttempts(first: 1) {
          edges {
            node {
              order {
                id
                name
                totalPrice {
                  amount
                  currencyCode
                }
              }
            }
          }
        }
      }
    }
  }
}
This is useful for displaying order history to customers in the buyer portal.

Upcoming Billing Cycles

The app calculates upcoming billing cycles to show customers when their next charges will occur:
// Query upcoming cycles within a date range
const startDate = DateTime.now().toISO();
const endDate = DateTime.now().plus({ months: 3 }).toISO();

const upcomingCycles = await admin.graphql(
  SubscriptionBillingCycles,
  {
    variables: {
      contractId,
      first: 10,
      startDate,
      endDate,
    },
  }
);

Billing Cycle Index

The cycle index is a zero-based counter that increments with each billing cycle:
  • First billing cycle: cycleIndex = 0
  • Second billing cycle: cycleIndex = 1
  • Third billing cycle: cycleIndex = 2
This index is used for:
  • Tracking dunning attempts per cycle
  • Applying cycle-based discounts
  • Displaying order history to customers
// Apply different discounts based on cycle
const cycleDiscounts = [
  {
    afterCycle: 0,
    adjustmentType: 'PERCENTAGE',
    adjustmentValue: { percentage: 20 }, // 20% off first 3 orders
  },
  {
    afterCycle: 3,
    adjustmentType: 'PERCENTAGE',
    adjustmentValue: { percentage: 10 }, // 10% off subsequent orders
  },
];

Best Practices

  1. Set appropriate billing hours: Schedule billing during times when your support team is available to handle issues
  2. Monitor timezone settings: Ensure billing schedules respect customer timezones for better experience
  3. Track cycle indices: Use cycle index for analytics and understanding subscription lifecycle patterns
  4. Handle skipped cycles: Display skipped cycles clearly to customers to avoid confusion
  5. Check ready status: Always verify billing attempts are ready before processing results

Error Handling

Billing attempts can fail for various reasons:
  • Payment method expired or declined
  • Insufficient inventory
  • Invalid shipping address
  • Network issues
The app handles these through Dunning Management, which automatically retries failed billing attempts.

Build docs developers (and LLMs) love