Skip to main content
Billing schedules control when subscription billing attempts are processed. The reference app provides flexible scheduling options to optimize billing times for different merchant needs.

Overview

The billing schedule system allows merchants to:
  • Configure billing to run at specific times
  • Set timezone-aware billing windows
  • Process billing attempts in batches
  • Optimize for payment success rates
Billing schedules are managed per shop and stored in the BillingSchedule database model.

How Billing Schedules Work

1

Schedule Activation

A billing schedule is created when the app is installed and activated for the shop
2

Timezone Detection

The system automatically detects the shop’s IANA timezone
3

Batch Processing

Active billing schedules are processed in batches by background jobs
4

Billing Execution

Subscription contracts are billed according to their billing policy and the schedule window

BillingSchedule Model

The BillingSchedule model manages scheduling configuration in app/models/BillingSchedule/BillingSchedule.server.ts:1-42:
export async function createActiveBillingSchedule(
  shop: string,
): Promise<BillingSchedule> {
  const timezone = await queryTimezone(shop);
  return await upsert({shop, active: true, timezone});
}

async function queryTimezone(shop: string): Promise<string> {
  const {admin} = await unauthenticated.admin(shop);
  const {
    shop: {ianaTimezone},
  } = await getShopInfos(admin.graphql);
  return ianaTimezone;
}

Model Fields

shop
string
required
The shop domain identifier
active
boolean
required
Whether billing processing is active for this shop
timezone
string
required
IANA timezone identifier (e.g., America/New_York, Europe/London)
hour
number
Optional hour of day (0-23) for billing processing

Timezone Handling

The app uses IANA timezone identifiers to ensure accurate billing times:

Automatic Detection

Shop timezone is queried from Shopify during setup

Daylight Saving Support

IANA timezones automatically handle DST transitions

Global Support

Works with all Shopify-supported timezones worldwide

Consistent Billing

Ensures billing occurs at predictable local times

Batch Processing

Billing schedules are processed in batches to handle large numbers of subscriptions efficiently:
export async function findActiveBillingSchedulesInBatches(
  callback: PaginatorCallbackFn,
): Promise<void> {
  await paginate({where: {active: true}}, callback);
}
The pagination system in app/models/BillingSchedule/paginate.ts processes active schedules in manageable chunks:
  • Queries active billing schedules
  • Processes in configurable batch sizes
  • Executes callback function for each batch
  • Handles errors gracefully without stopping the entire process
Batch processing prevents memory issues when dealing with shops that have thousands of active subscriptions.

Billing Policy Configuration

Each subscription contract has a billing policy that defines:

Interval Types

interval
enum
The billing frequency unit
  • DAY - Daily billing
  • WEEK - Weekly billing
  • MONTH - Monthly billing
  • YEAR - Yearly billing
intervalCount
number
How many intervals between billing attempts (e.g., every 2 weeks)

Example Billing Policies

// Weekly subscription
{
  interval: "WEEK",
  intervalCount: 1
}

// Bi-monthly subscription
{
  interval: "MONTH",
  intervalCount: 2
}

// Quarterly subscription
{
  interval: "MONTH",
  intervalCount: 3
}

Billing Cycle Management

The app calculates upcoming billing cycles based on:
  1. Contract billing policy (interval and interval count)
  2. Current billing schedule status
  3. Timezone configuration
  4. Previous billing attempt history
From app/routes/app.contracts.$id._index/route.tsx:74-92:
const upcomingBillingCyclesPromise =
  subscriptionContract.status !== SubscriptionContractStatus.Cancelled
    ? getNextBillingCycleDates(
        admin.graphql,
        gid,
        billingCyclesCount,
        interval,
        intervalCount,
      )
    : {
        upcomingBillingCycles: [],
        hasMoreBillingCycles: false,
      };

Schedule Activation Flow

Optimizing Billing Times

Consider these factors when configuring billing schedules:

Payment Success Rates

Schedule billing during times when customers have sufficient funds

Time Zone Awareness

Align billing with customer’s local time to improve success rates

Processing Capacity

Distribute billing across hours to avoid overwhelming payment processors

Customer Experience

Avoid billing during inconvenient hours (e.g., late night)

Database Schema

The BillingSchedule table structure:
model BillingSchedule {
  id        String   @id @default(cuid())
  shop      String   @unique
  active    Boolean
  timezone  String
  hour      Int?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Managing Schedule Status

Billing schedules can be activated or deactivated:
When activated, the shop’s subscriptions will be processed according to the configured timezone and hour.
await createActiveBillingSchedule(shop);
Deactivating prevents automatic billing processing without deleting the schedule configuration.
await prisma.billingSchedule.update({
  where: {shop},
  update: {active: false}
});

Best Practices

1

Set Appropriate Timezones

Always use the shop’s local timezone for better customer experience
2

Monitor Batch Processing

Track batch processing performance to ensure timely billing execution
3

Handle Failed Schedules

Implement retry logic for schedules that fail to process
4

Test Timezone Changes

Verify billing times when shops change their timezone configuration
Changing a shop’s timezone affects when future billing attempts occur. Communicate this change to customers if billing times shift significantly.

Build docs developers (and LLMs) love