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 arecurrence (inherited from the plan price or provided inline). It
defines the cycle length and the alignment point for invoices.
Unit multiplier. Positive integer. E.g.:
interval=3 + unit=month → every 3 months.Cycle unit. One of:
day, week, month, year.How the cycle aligns to a reference point. One of:
subscription_start,
day_of_month, end_of_month. Detailed below.Required when
anchor='day_of_month'. Day of the month, range 1 to 31. Ignored
for other anchors.When to charge within the cycle:
prepaid (at the start) or postpaid (at the end).
Detailed in Prepaid vs Postpaid.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.subscription_start — aligns to the start date
subscription_start — aligns to the start date
The next cycle is simply
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
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):| Cycle | Start | End (= next invoice) |
|---|---|---|
| 1 | Jan 15 10:00 | Feb 15 10:00 |
| 2 | Feb 15 10:00 | Mar 15 10:00 |
| 3 | Mar 15 10:00 | Apr 15 10:00 |
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.day_of_month — every Xth day of the month (with clamp)
day_of_month — every Xth day of the month (with clamp)
Always charges on
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.
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 start | End of 1st cycle (1st recurring invoice) |
|---|---|
| Apr 05 | May 10 |
| Apr 19 | May 10 |
| Apr 10 (already on anchor day) | May 10 |
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.end_of_month — always the last day of the month
end_of_month — always the last day of the month
Always charges on the last day of each month in the cycle, regardless of its length.Example (
Just like
unit=month, interval=1, start on Jan 10):| Cycle | End (= next invoice) |
|---|---|
| 1 | Feb 28 (or Feb 29 in a leap year) |
| 2 | Mar 31 |
| 3 | Apr 30 |
day_of_month, the first recurring invoice falls one interval ahead, never in
the starting month.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.
collectionTimingat the subscription level —prepaid(default) orpostpaid.collectionTiminginsiderecurrenceitself — when absent, defaults toprepaid(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.
| Mode | When invoices are created | Use case |
|---|---|---|
just_in_time (default) | 1 invoice per cycle, generated by the scheduler at each cycle turn | Open-ended subscriptions (SaaS/streaming) and contracts with no need to see future cycles |
upfront | All maxCycles invoices at creation time, each with a distinct dueAt | Fixed-term contracts (e-learning, installment plans) where the merchant wants cash flow predictability |
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.
Trial duration in days. Integer
>= 0.Whether the trial requires a payment method to be attached before it can start.
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.
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):
Accepted range: 0 to 30 days. When omitted, it is automatically derived from the
type of the default payment method (
defaultPaymentMethodRef.type).| Payment method | Default lead time | Why |
|---|---|---|
card | 0 days | Instant charge |
pix | 1 day | Time to send the QR Code to the customer |
boleto | 2 days | Time for the banking network to register the boleto |
other | 0 days | No 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.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)
| Field | Default | Range / values | Meaning |
|---|---|---|---|
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 |
maxRetries | 3 | 0 to 10 | Maximum number of retries before dunning is exhausted |
dunningFinalPolicy | mark_unpaid | mark_unpaid or cancel | What to do when dunning is exhausted |
hardDeclineCategories | ['hard_decline', 'authentication_required'] | subset of soft_decline, hard_decline, insufficient_funds, authentication_required, other | Failure 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.
Charge fails
The invoice moves from
open to past_due. The attempt is recorded as failed with
the failure category.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.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.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.retryIntervalsDays=[3,5,7], maxRetries=3,
dunningFinalPolicy=mark_unpaid), initial charge failing on Jun 01:
| Attempt | attemptNumber | retryCountSoFar | Date | Result |
|---|---|---|---|---|
| Initial | 1 | 0 | Jun 01 | Fails → past_due, schedules retry +3d |
| Retry 1 | 2 | 1 | Jun 04 | Fails → schedules retry +5d |
| Retry 2 | 3 | 2 | Jun 09 | Fails → schedules retry +7d |
| Retry 3 | 4 | 3 | Jun 16 | Fails → retryCountSoFar (3) >= maxRetries (3): exhausted → subscription becomes unpaid |
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.

