Recurring invoices
An invoice that repeats (a subscription, a retainer, rent, a service contract billed monthly) is written down once as a recurring invoice: the customer, the lines, how often, from when until when, the payment terms. A second person approves those terms once. After that a person runs it on demand, and each run makes the invoice of every period that has fallen due, as an ordinary invoice. The design is ADR-0027 (section 5, slice 2); the controls are in the control matrix (section "Recurring invoices", RV1 to RV9).
Everything below is a REST operation first (/v1, tag "recurring invoices" in the
generated reference); the web UI (Sales → Recurring Invoices, on
the Finance menu next to Customer Invoices) and the MCP read tools call the same operations.
Runs are on demand. Nothing here runs by itself: no scheduler, no timer. A cue on the Role Center shows
how many recurring invoices are due, it opens the list of them, and a person presses Make due
invoices (or calls POST /v1/recurring-invoices/run). A scheduler is new infrastructure and needs its own
ADR (ADR-0029); the close checklist warns when a run was
missed (below).
The template
On the form each field says what goes in it. A new template is named after its customer and frequency ("Muster AG, monthly") until you type a name, and its currency and payment terms (10, 30 or 60 days) are one click away.
| Field | Meaning |
|---|---|
entity, party |
The entity that issues the invoices and the customer (who must be active before the template can be activated: a draft customer cannot be invoiced) |
name |
What the template is called in lists. Not printed, and not part of the terms |
currency |
ISO 4217; default the customer's |
lines |
The lines of every invoice, exactly as on POST /v1/documents (description, quantity, unit price, account, VAT rate type, supply type, ...), but each states its description and unit price even when it names a product: they are part of the approved terms (422 recurring_line_incomplete). VAT is determined for each invoice on its own date, by the same rules as for an invoice entered by hand |
frequency |
monthly, quarterly (every 3 months) or yearly |
start_date |
The first due date, and the date of the first invoice. It may lie in the past |
end_date |
The last day an invoice may fall due, inclusive; none for no end |
payment_terms_days |
Days from the invoice date to its due date; none: the customer's own terms, as on any invoice |
auto_submit |
false (default): each invoice is left as a draft for a person to submit. true: the system submits it as it is made, through the approval policy |
notes |
Free text printed on every invoice |
next_due |
The next due date an invoice is to be made for; the start date at first, then the date after each period made. None once the template has ended |
status |
draft, pending_approval, active, paused, ended |
Creating a template builds its first invoice without keeping it, so a missing account, an unknown customer or a tax decision that cannot be made is refused at once (404, 422), not at the first run.
The due dates
The due dates are the start date and every 1, 3 or 12 months after it, counted from the start date (never from the previous due date), each keeping the start date's day of the month. A month that is too short takes its last day, and the day comes back in the next month that has it:
| Start | Frequency | Due dates |
|---|---|---|
| 31 Jan 2026 | monthly | 31 Jan, 28 Feb, 31 Mar, 30 Apr, 31 May ... (a bill for the end of every month starts on the 31st) |
| 29 Jan 2026 | monthly | 29 Jan, 28 Feb, 29 Mar ... |
| 29 Feb 2024 | yearly | 29 Feb 2024, 28 Feb 2025, 28 Feb 2026, 28 Feb 2027, 29 Feb 2028 |
| 30 Nov 2026 | quarterly | 30 Nov, 28 Feb 2027, 30 May, 30 Aug, 30 Nov |
The end date is inclusive: the last due date is the last one on or before it, and after it the
template ends by itself (status ended, no next_due). An end date moved to before the next due
date (a change of terms like any other) ends the template at once, withdrawing a request for approval:
it is ended in the list, on the card and in the due list, not an active template that never runs. A start on the 28th of February is the 28th of
every month: there is no "last day of the month" rule beyond the clamping above. The pure functions are
domain/schedule and are tested for month ends, the leap day, quarters, years and the end date
(tests/unit/test_recurring_schedule.py).
Activation: a second person approves the terms
A template is created as a draft by anyone who may draft, an agent too. Making it run is a human decision:
POST /v1/recurring-invoices/{id}/activateasks for it. The policy actionactivate_recurring_invoicedenies agents (403, naming the ruleagents_no_revenue_controls), whatever an organization's own approval policy says.- For a human the default rule is
require_approval: the template becomespending_approvaland an approval request (object typerecurring_invoice) waits for a second person (POST /v1/approvals/{approval_id}/approve, or Approve on the card). Never a person who prepared the terms (created the template, or changed any of the terms since: the template keeps them all interms_changed_by) or who asked for the approval, whichever key or session they use; a key the system issued belongs to no person and cannot decide. So A creates, B edits the lines, A asks: B cannot approve what B wrote, and a third person does. - The approval is bound to the terms. The request carries the SHA-256 of the terms (customer,
currency, frequency, dates, payment terms,
auto_submit, notes, lines), and so does the template when it is approved (terms_digest). If the terms differ when the approval is decided the answer is409 terms_changedand nothing is activated. The request says on its face whether the invoices are submitted automatically (auto_submit), the net amount of one invoice and the frequency. - An organization with one person allows the action in its approval
policy (
PUT /v1/approval-policy, versioned and audited): a human with the approve scope then activates it at once.
A change to the terms stops the template. PATCH /v1/recurring-invoices/{id} changes any field. A
change to the name changes nothing else. A change to anything else pauses an active template (it
makes no invoice until a second person approves the new terms), takes a template that waits for approval
back to draft and withdraws the request, and leaves a paused one paused. Three layers keep this true: the
service, the run (it works only a template whose current terms have the approved digest) and a database
trigger that refuses a change to the terms of an active template at all. The frequency and the start
date cannot change once an invoice was made from the template (409): end it and create a new one.
Pausing and resuming. POST .../pause (a reason, kept in the audit trail) stops an active template;
anyone who may draft can, because it makes nothing happen. Activating a paused template whose terms are
still the approved ones resumes it at once: a second person already approved exactly these terms. A
paused template whose terms changed is asked for again.
The run
POST /v1/recurring-invoices/run with {"through": "2026-11-30"} (and optionally entity or one
template) makes, for every active template with a due date on or before through, the invoice of each
due date, oldest first, dated that due date. Needs the draft scope and an Idempotency-Key.
- A generated invoice is an ordinary invoice. It is made by
trade.create_document, the function an invoice entered by hand goes through, with the template's lines, currency and payment terms: same tax determination, same totals, same legal notes, same due date rule, no number yet. The one difference is where it came from:provenance.recurring_invoicenames the template and the due date, and the run record links both ways. A test makes the same invoice by hand and compares every column and every line. - Draft or submitted. With
auto_submit = falseit stays a draft; a person submits it through the approval policy like any invoice. Withauto_submit = truethe system actor submits it through the normal path (trade.submit_document: the approval policy'ssystem_allowedrule, the open period, the gap-free number, the hash chain), which the approved terms authorise; the journal entry's creator is the system, the invoice's creator the person who ran it. Nothing posts around the choke point. - Idempotent. A repeated run makes nothing: the template's next due date has moved on. A retried
request with the same
Idempotency-Keyreplays the first answer. Simultaneous runs queue on the template's row: each template is locked and read again before it is worked, so the second run sees the dates the first advanced and finds nothing left. AndUNIQUE (template_id, due_date)on the run table makes a second invoice for one period impossible even if the application were bypassed. - A failure stops the template, not the run. A period that cannot be made (a closed period under
auto_submit, an inactive customer, an inactive account) is recorded asfailedwith its error; everything the attempt did is undone (a savepoint), the template stays at that date (a later period is never made before an earlier one), the other templates carry on, and the next run tries the failed period again. The run's answer lists each period asdrafted,posted,pending_approvalorfailed. Only a refusal the system names (anErpError) is caught per period. Any other exception aborts the whole run atomically: nothing is made and nothing is recorded, and the run can simply be repeated. - Lock order. Templates are worked entity by entity, then by id, so the posting locks of an entity
(
auto_submit) are taken in the order of the entity ids (ADR-0029, X3). - A year at most. One run makes at most twelve due dates per template, so a start date far in the past
is worked off in steps rather than in one flood of back-dated invoices;
more_duesays to run again. - Not ahead of time.
throughmay not lie more than a day after the server's date (UTC), so a person's own date is always accepted and invoices are never made for periods that have not come.
A voided generated draft stays a record of its period: the period is not made again. A corrected invoice for it is entered by hand.
The due cue, the due list and the close check
The templates a run through a day would make invoices for are the ones that are active, with a next due
date on or before that day (and not after the end date). One rule, recurring_state.due_condition,
serves three views, so the number a tile shows is the number of rows it opens:
- the Role Center cue Recurring invoices due (
recurring_dueinGET /v1/entities/{entity}/overview), which opens/recurring-invoices?due=1; - the due list,
GET /v1/recurring-invoices?due_through=<day>; - the close check
recurring_generated(a warning, never a failure): on an open period it counts the templates due through the period's last day and links to the due list. Not applicable while the entity has no active recurring invoice.
The audit trail
Every step is an audit event on the template (GET /v1/audit-events?object_id={id}): recurring_invoice.create,
.update (what changed, whether the terms changed, whether it was paused), approval.request,
recurring_invoice.activate (who approved which terms_digest, who asked, and whether it only resumed),
approval.approved or .rejected, .pause (the reason), .run (one per period made or failed: the due
date, the outcome, the invoice, the error) and .end. The submission of an invoice by the system is the
ordinary document.post event of that invoice. The run records (recurring_invoice_run) are evidence:
a row that made an invoice never changes, only a failed attempt is replaced by its retry, and nothing is
deleted (a trigger). A template is never deleted either; it ends.
REST operations
| Operation | |
|---|---|
POST /v1/recurring-invoices |
Create a draft (draft scope, Idempotency-Key) |
GET /v1/recurring-invoices |
List; filters entity, status, party, due_through, limit |
GET /v1/recurring-invoices/{id} |
One template, with terms_approved, the pending approval and the number of invoices made |
PATCH /v1/recurring-invoices/{id} |
Change it (see "a change to the terms stops the template") |
POST /v1/recurring-invoices/{id}/activate |
Ask for the terms to be approved, or resume. Humans only |
POST /v1/recurring-invoices/{id}/pause |
Pause with a reason |
POST /v1/recurring-invoices/run |
Make the invoices that are due (draft scope, Idempotency-Key) |
GET /v1/recurring-invoices/{id}/runs |
Every period made or tried, with the invoice and its status now |
The MCP tools list_recurring_invoices, get_recurring_invoice and recurring_invoice_runs read them;
activating and running are people's decisions and are not tools.
# create (a draft), ask for approval; a second person approves; then run
curl -X POST "$ERP_URL/v1/recurring-invoices" \
-H "Authorization: Bearer $ERP_KEY" -H "Idempotency-Key: rec-1" -H "Content-Type: application/json" \
-d '{"entity": "CH01", "party": "C-CH", "name": "Hosting and support, monthly",
"frequency": "monthly", "start_date": "2026-11-01", "payment_terms_days": 30,
"lines": [{"description": "Hosting and support", "quantity": "1", "unit_price": "450.00"}]}'
# -> 201 {"id": "...", "status": "draft", "next_due": "2026-11-01", "net_total": "450.00", ...}
curl -X POST "$ERP_URL/v1/recurring-invoices/$ID/activate" -H "Authorization: Bearer $PERSON_KEY"
# -> {"policy": "require_approval", "approval_id": "...", "template": {"status": "pending_approval", ...}}
curl -X POST "$ERP_URL/v1/approvals/$APPROVAL_ID/approve" -H "Authorization: Bearer $SECOND_PERSON_KEY" \
-H "Content-Type: application/json" -d '{}'
curl -X POST "$ERP_URL/v1/recurring-invoices/run" \
-H "Authorization: Bearer $ERP_KEY" -H "Idempotency-Key: run-2026-11-30" -H "Content-Type: application/json" \
-d '{"through": "2026-11-30", "entity": "CH01"}'
# -> {"through": "2026-11-30", "made": 1, "failed": 0, "more_due": false,
# "items": [{"due_date": "2026-11-01", "status": "drafted", "document_id": "...", ...}]}
The screens
Sales → Recurring Invoices (/recurring-invoices) lists the templates with customer, frequency, next
due date, net amount per invoice, submission and status; filter by status or Only those due (the due
list the cue counts); Make due invoices runs through today and says how many were made and how many
failed. New recurring invoice is the form (customer, frequency, dates, payment terms, automatic
submission, notes, lines). The card shows the terms, who approved them and their fingerprint, the
lines and the invoices made (each opens its invoice), with the actions the status allows: Ask for
approval, Approve and Reject for a second person, Make due invoices, Edit (the form warns
that changing an active template pauses it), Pause, Resume. The Approvals page shows the request
with the terms and, when it is so, "Each invoice is submitted automatically."
What this does not do, and where the ADR left a choice
- No scheduler (founder's decision, ADR-0029): runs are on demand.
- No contract billing yet. A template whose lines name a performance obligation bills a revenue
contract (ADR-0027, section 5, last bullet); the contracts are another slice. The nullable
obligation_idof the invoice line is not added here: no code would read it, andtrade_document_lineis a hot spot (ADR-0029); the template's lines areDocumentLineInputdocuments, so the field joins them when the contracts slice adds it. - No
timing(advance or arrears). The ADR lists it for contract billing; without contracts it would change nothing, so the column is left out until the slice that uses it. The invoice date is the due date. - No text placeholders (for example the month in a line description) and no
If-Matchversion on templates: a change locks the template's row, and its terms must be approved again anyway. - Month ends are the clamping rule above, not "the last day of the month".
- The twelve-per-run cap and the one-day allowance on
throughare choices of this slice (the conservative option), not of the ADR.