Skip to main content
This page is conceptual: it explains how the recurring billing engine decides when each invoice is created and when it is charged. The fields referenced here are the same ones you send when creating a subscription — see Subscriptions for the full request and Invoices for the lifecycle of each invoice.
All dates inside the engine are normalized to UTC. The cycle calculation is deterministic: it depends only on the start date and the recurrence rule (recurrence), with no external clock. In transport (body/query), dates always travel as ISO 8601 with offset — see Conventions.

The recurrence rule

Each subscription has a recurrence (inherited from the plan price or provided inline). It defines the cycle length and the alignment point for invoices.
interval
integer
required
Unit multiplier. Positive integer. E.g.: interval=3 + unit=month → every 3 months.
unit
string
required
Cycle unit. One of: day, week, month, year.
anchor
string
required
How the cycle aligns to a reference point. One of: subscription_start, day_of_month, end_of_month. Detailed below.
anchorDay
integer
Required when anchor='day_of_month'. Day of the month, range 1 to 31. Ignored for other anchors.
collectionTiming
string
default:"prepaid"
When to charge within the cycle: prepaid (at the start) or postpaid (at the end). Detailed in Prepaid vs Postpaid.
The day_of_month and end_of_month anchors only make sense with unit='month' or unit='year'. Combining them with day/week is rejected at validation (“every 1 day, on the 15th” has no valid semantics).

Anchors

The anchor determines which date the end of the current cycle falls on — which is also the start of the next cycle and, by default, the date of the next invoice.
The next cycle is simply start + interval. The time-of-day (hour/min/sec) of the start date is preserved.Example (unit=month, interval=1, start on Jan 15, 2026 10:00):
CycleStartEnd (= next invoice)
1Jan 15 10:00Feb 15 10:00
2Feb 15 10:00Mar 15 10:00
3Mar 15 10:00Apr 15 10:00
For short months the end-of-month clamp applies (see below): starting on Jan 31 → the next end falls on Feb 28 (or Feb 29 in a leap year). Unlike day_of_month, the subscription_start anchor does not re-anchor to the original day: each step advances from the previous cycle end (already clamped), so after Feb 28 the next cycle ends on Mar 28, then Apr 28 — the day “shrinks” to fit the shortest month crossed and never bounces back to 31.
Always charges on anchorDay. The first recurring invoice falls at least one interval ahead — never in the month the subscription starts. This prevents short cycles at launch.Example (unit=month, interval=1, anchorDay=10):
Subscription startEnd of 1st cycle (1st recurring invoice)
Apr 05May 10
Apr 19May 10
Apr 10 (already on anchor day)May 10
That is: purchasing on the 5th with an anchor on the 10th does not charge on the 10th of the same month — it moves to the following month.
If anchorDay is greater than the number of days in the target month, the engine adjusts to the last day of the month (consistent with Stripe behavior). anchorDay=31 in February becomes Feb 28 (or Feb 29 in a leap year). And since the alignment re-anchors to the original day at each step, there is no drift: after Feb 28 (31 clamped), the next cycle goes back to Mar 31.
Always charges on the last day of each month in the cycle, regardless of its length.Example (unit=month, interval=1, start on Jan 10):
CycleEnd (= next invoice)
1Feb 28 (or Feb 29 in a leap year)
2Mar 31
3Apr 30
Just like day_of_month, the first recurring invoice falls one interval ahead, never in the starting month.
The time-of-day (hour/minute/second) from the start date is preserved as cycles advance. The clamp only adjusts the day, never the clock.

Prepaid vs Postpaid

collectionTiming defines where within the cycle the charge occurs.

prepaid (default)

Charges at the start of each period. This is the classic SaaS/streaming model: you pay for the period that is beginning.

postpaid

Charges at the end of each period. Consumption/utility model: you pay for the period that has already passed.
In the subscription creation request there are two places that handle timing:
  • collectionTiming at the subscription level — prepaid (default) or postpaid.
  • collectionTiming inside recurrence itself — when absent, defaults to prepaid (backwards compatibility with older prices that did not have this field).
The absence of this field always means prepaid. If you do not set anything, your subscriptions will charge at the start of the cycle.

Invoice generation: just_in_time vs upfront

invoiceGenerationMode controls how many invoices are created and when.
ModeWhen invoices are createdUse case
just_in_time (default)1 invoice per cycle, generated by the scheduler at each cycle turnOpen-ended subscriptions (SaaS/streaming) and contracts with no need to see future cycles
upfrontAll maxCycles invoices at creation time, each with a distinct dueAtFixed-term contracts (e-learning, installment plans) where the merchant wants cash flow predictability
invoiceGenerationMode='upfront' requires maxCycles to be set (it cannot be null). Without it, subscription creation is rejected at validation.
In just_in_time mode, the total number of cycles is capped by maxCycles (when provided); without maxCycles, the subscription is considered perpetual and the scheduler continues generating one invoice per cycle indefinitely.

Trial period

trialSpec defines an initial period without any charge.
trialSpec.durationDays
integer
Trial duration in days. Integer >= 0.
trialSpec.requiresPaymentMethod
boolean
Whether the trial requires a payment method to be attached before it can start.
During the trial the subscription stays in trialing status and no invoice is charged. At the end of the trial, the engine generates the first invoice and the subscription enters normal billing.
trialSpec and bootstrapPayment are mutually exclusive. A trial implies that cycle 1 has not yet been charged (the end of the trial is what generates the 1st invoice); bootstrapPayment means cycle 1 was already paid externally. Sending both together is rejected.
The trialSpec at the subscription level takes precedence over the trial derived from plan-based items (which comes from the price). Use the subscription-level override when the trial is determined outside the plan — for example, an inline checkout with items that have no associated price.

chargeLeadTimeDays — charge advance notice

chargeLeadTimeDays is the number of days between when the invoice is triggered for collection (chargeAt — when the invoice opens and the boleto/PIX is created in the PSP) and its due date (dueAt):
chargeAt = dueAt − chargeLeadTimeDays
chargeLeadTimeDays
integer
Accepted range: 0 to 30 days. When omitted, it is automatically derived from the type of the default payment method (defaultPaymentMethodRef.type).
Defaults derived by payment method type:
Payment methodDefault lead timeWhy
card0 daysInstant charge
pix1 dayTime to send the QR Code to the customer
boleto2 daysTime for the banking network to register the boleto
other0 daysNo advance needed
The resolution rule is: an explicit value always wins; otherwise, it derives from the payment method type; if neither is present, the default is 0. Persisted on the subscription as chargeLeadTimeDays.
Example: a boleto invoice due on Jun 20 (dueAt), with a lead time of 2, is triggered for collection on Jun 18 (chargeAt) — giving 2 days for the boleto to circulate before the due date.

Dunning — what happens when a charge fails

When a recurring charge fails, the engine enters dunning: it decides whether to schedule a new retry or exhaust the attempts and apply the final policy. The behavior is configurable per merchant via BillingSettings.

Configuration (BillingSettings)

FieldDefaultRange / valuesMeaning
retryIntervalsDays[3, 5, 7]array of integers >= 0 (up to 10)Days to wait before each retry. If the index is exceeded, the last interval is reused
maxRetries30 to 10Maximum number of retries before dunning is exhausted
dunningFinalPolicymark_unpaidmark_unpaid or cancelWhat to do when dunning is exhausted
hardDeclineCategories['hard_decline', 'authentication_required']subset of soft_decline, hard_decline, insufficient_funds, authentication_required, otherFailure categories that skip retry and go directly to the final policy

How the dunning sequence works

The count follows Stripe’s model: attempt #1 is the initial charge; retries start at 2. Therefore, retryCountSoFar = attemptNumber − 1.
1

Charge fails

The invoice moves from open to past_due. The attempt is recorded as failed with the failure category.
2

Are there retries remaining?

If retryCountSoFar < maxRetries and the failure is not a hard decline, the engine schedules the next attempt: nextRetryAt = failure time + retryIntervalsDays[index]. The subscription remains active and the invoice stays in past_due.
3

Dunning exhausted

Exhausted when retryCountSoFar >= maxRetries or the failure falls into a hardDeclineCategories category (which skips retry immediately). At that point nextRetryAt becomes null and the final policy is applied.
4

Final policy

With dunningFinalPolicy = mark_unpaid (default), the subscription is marked as unpaid. With cancel, the subscription is automatically canceled.
The default subscription transition when a charge fails is active/charging → past_due (retrying) → unpaid (dunning exhausted with mark_unpaid policy). With the cancel policy, the final outcome is canceled instead of unpaid.
Concrete example (defaults: retryIntervalsDays=[3,5,7], maxRetries=3, dunningFinalPolicy=mark_unpaid), initial charge failing on Jun 01:
AttemptattemptNumberretryCountSoFarDateResult
Initial10Jun 01Fails → past_due, schedules retry +3d
Retry 121Jun 04Fails → schedules retry +5d
Retry 232Jun 09Fails → schedules retry +7d
Retry 343Jun 16Fails → retryCountSoFar (3) >= maxRetries (3): exhausted → subscription becomes unpaid
Enrollment invoices (kind=enrollment) do not enter dunning: the failure is terminal, with no retry. The invoice moves from open to past_due (to prevent it from being retried every hour) and the enrollment flow decides on cancellation/closure of the subscription.
For detailed subscription statuses (incomplete, trialing, active, past_due, unpaid, paused, canceled, completed) see Subscriptions. For API call error codes see Errors.

See also

Subscriptions

Create and manage subscriptions — full request with recurrence, trialSpec, collectionTiming and other fields.

Plans and prices

Where recurrence is inherited from when the subscription is plan-based.

Invoices

Lifecycle of each invoice — open, past_due, paid and the chargeAt/dueAt fields.

Subscriptions overview

Concepts of Z2Pay’s recurring billing module.