Bill Runs and Schedules
A bill run is the monthly batch wrapper around the per-tenant invoice lifecycle. It is restartable, and a tenant that cannot be billed never stops the rest.

Stages
1. Prepare — per tenant: generate recurring charges from snapshots, gather usage, compute tax, build a draft, evaluate guards. Tenants that cannot be billed are marked blocked and reported. They consume no invoice number.
2. Finalize — finalizes every unblocked draft sequentially, because invoice numbers are fleet-wide and gapless. Guards are re-checked here, so a blocker that appeared after prepare still stops that tenant.
3. Send — emails each finalized invoice to that tenant's billing recipients, with the call-detail CSV attached and a live share link. Per-invoice send state, so a retry re-sends only what failed. Re-sending an already-sent invoice requires an explicit force and is audited.
4. Push — hands finalized invoices to the connected accounting package. See Xero and QuickBooks Online.
Each stage can be run on its own. A run that stops at prepare leaves drafts, which is the correct configuration for a first month.
Pacing and ceilings
Accounting APIs have daily call limits, and a fleet-wide push can exhaust one. A run therefore paces itself:
- Day budget — how many accounting calls this run may spend today
- Ceiling — the hard per-day limit for the provider
- Pacing — the rate at which pushes are issued
A run that reaches its budget stops cleanly and reports what remains, rather than failing halfway through with an unclear state. Resume it the next day.
Xero's documented daily ceiling is 1,000 calls, not 5,000. At roughly three calls per pushed tenant, that is about 300 tenants per organisation per day.
Push blockers
A push is refused, locally and before any API call, when:
- The invoice is not finalized
- The invoice has already been pushed
- A sibling invoice for the same tenant and period has already been pushed
- The tenant has no accounting customer mapping
- The invoice has no due date
- The connection's tokens have expired and cannot refresh
Because a second push is refused locally, there is no re-push button. See the Xero page for why that matters.
Schedules
Schedules run bill runs on a clock.
- Day of month — limited to 1–28, since later days do not exist in every month
- Hour — checked hourly
- Timezone — chosen from a list of common zones
- Stages — prepare always; finalize, email and export as ticked
A schedule fires once per billing period. The period it last ran is stored and compared before anything is created, so a container restart inside the firing hour cannot produce a second run or a second set of invoice numbers.
Scheduler ticks run hourly from container boot. A schedule set for 09:00 fires somewhere within that hour, depending on when the container last started. It will not fire before 09:00.
All three optional stages unticked produces drafts only. That is the right setting for the first month of a new install, and for the whole parallel-run period.
Reading a run
The run detail shows, per tenant: stage reached, invoice number if allocated, blockers if any, send state, push state and export state.
If every tenant in a run is blocked, the run reports the sent stage with a
timestamp even though nothing was sent. Check the per-tenant results rather
than the run's stage badge alone.
Worked examples
A drafts-only run
The right configuration for a first month, and for the whole parallel-run period:
Bill run 2026-08 stages: prepare only
hq.northwind-example.com DRAFT 804.57 + 82.72 = 887.29
west.northwind-example.com DRAFT 1,108.00 + 96.60 = 1,204.60
branch.northwind-example.com BLOCKED no sell plan
east.northwind-example.com BLOCKED unreviewed trunk: carrier-secondary
2 drafts, 2 blocked, 0 finalized, 0 numbers consumed
No invoice numbers were used. Fix the two blockers and re-run prepare as many times as you like.
The same run, finalized and sent
Bill run 2026-08 stages: prepare, finalize, email
prepare 4 tenants -> 4 drafts, 0 blocked
finalize sequential (numbers are fleet-wide and gapless)
hq INV-00151 887.29
west INV-00152 1,204.60
east INV-00153 531.40
south INV-00154 642.15
email hq sent accounts@customer-example.com
west sent ap@west-example.com
east FAILED SMTP timeout
south sent billing@south-example.com
Status: 4 finalized, 3 sent, 1 send failure
Retrying the send stage re-sends only east. Send state is per invoice.
Re-sending an invoice that already sent requires an explicit force, and is audited.
A push that hits the Xero ceiling
Bill run 2026-08 stage: push (Xero, org "Northwind Telecom")
Day budget: 1,000 calls Consumed: 996 Remaining: 4
Pushed: 331 tenants
Remaining: 68 tenants — budget exhausted, resume tomorrow
Blocked: 3 tenants
acme-east no customer mapping
acme-west already pushed
acme-south no due date
The run stopped cleanly and said what is left. At roughly three API calls per tenant, 1,000 calls is about 330 tenants per organisation per day.
A schedule
| Field | Example |
|---|---|
| Day of month | 1 |
| Hour | 3 |
| Timezone | America/New_York |
| Prepare | always |
| Finalize | ✓ |
| ✓ | |
| Export | ✓ |
2026-09-01 03:00 schedule fired for period 2026-08
2026-09-01 04:00 schedule tick — period 2026-08 already run, skipped
2026-09-01 05:00 schedule tick — period 2026-08 already run, skipped
The period it last ran is stored and compared before anything is created. A container restart inside the firing hour cannot produce a second run or a second set of invoice numbers.
For the first month, leave finalize, email and export unticked. That produces the drafts-only run above.