StabisoDocs

Autopilot and routines

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:

  1. POST /v1/recurring-invoices/{id}/activate asks for it. The policy action activate_recurring_invoice denies agents (403, naming the rule agents_no_revenue_controls), whatever an organization's own approval policy says.
  2. For a human the default rule is require_approval: the template becomes pending_approval and an approval request (object type recurring_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 in terms_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.
  3. 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 is 409 terms_changed and 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.
  4. 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_invoice names 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 = false it stays a draft; a person submits it through the approval policy like any invoice. With auto_submit = true the system actor submits it through the normal path (trade.submit_document: the approval policy's system_allowed rule, 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-Key replays 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. And UNIQUE (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 as failed with 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 as drafted, posted, pending_approval or failed. Only a refusal the system names (an ErpError) 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_due says to run again.
  • Not ahead of time. through may 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_due in GET /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_id of the invoice line is not added here: no code would read it, and trade_document_line is a hot spot (ADR-0029); the template's lines are DocumentLineInput documents, 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-Match version 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 through are choices of this slice (the conservative option), not of the ADR.
Stabiso, the Enterprise System of Record · generated from the code and its guides · release 6f03a5e846b3