ReferenceView as Markdown ↗

Invoice send: gates and guards

In plain terms

Sending an invoice normally takes three steps, with you approving the last one. Some clients are set to bill themselves the moment a job is finished, and those invoices go without that approval. Either way the same tax check has to pass first, and an invoice that fails it is never sent.

Operator-approved invoice sends run as three separate calls, with the jurisdiction validator as a hard gate on the third. A second caller — a client set to invoice on completion — sends with no pending operation and no approval leg, through the same validator.

At a glance

Who it's for:
Engineers building on Biloh
What a person approves:
Biloh never moves money: its tools record payments that already happened, and a pay-now link opens the business's own Stripe checkout for its client.

The three legs

An invoice send that an operator approves is deliberately not one-shot. Three separate calls, each with a distinct job:

LegCallWhat it doesWhat it does not do
1propose_send_invoiceCreates a pending operation, resolves recipients, renders the PDF and email preview, runs the duplicate guardSend anything. Change the invoice
2confirm_pending_operationMoves the operation to staged, awaiting operator approvalSend anything
3approve_send_invoiceRuns the jurisdiction validator, transitions the invoice to sent, dispatches the email, emits invoice.sent—

The invoice stays in draft and remains fully editable until leg 3 succeeds. Between legs 1 and 3 you can personalise the email — see Personalise a staged send.

Resends are two-leg, not three. propose_resend_invoice → confirm_pending_operation delivers immediately, because the original send was already operator-approved. A resend is pure re-delivery: no status change, no number change, no new postings.

The other caller: a send with no operator leg

The three legs above are the operator's chain, and they are not the only way an invoice is sent. A client can be set to invoice on completion, and that path sends with no pending operation and no approval leg at all — marking the visit complete is the only human act in it. A builder who reads the table above and assumes a staged operation always exists will be wrong for those clients.

What the two paths share, and what they do not:

Operator chainSend on completion
Pending operationYes, legs 1–3None
Who approvesThe operator, at leg 3Nobody; the client's setting is the standing approval
Jurisdiction validatorRuns, hardRuns, hard — the same gate
Duplicate-invoice guardRuns at propose timeNot on this path; see below
Recipient resolutionreceives_invoices contacts, then billing_emailIdentical
invoice.sentEmittedEmitted

The validator is the part to be precise about. It is one gate with one entry point, and the approval-free path does not get a gentler version of it — a send with nobody watching is the last place you would want a weaker tax check. What differs is only what happens on a refusal. There is no operator at the other end to hand a structured failure to, so nothing is sent, the visit is left waiting for a person with the reason recorded against it, and the client is never emailed an invoice that would have failed validation.

The propose-time duplicate guard is absent here because there is no propose leg to run it in. What protects this path is further upstream and stricter: one live invoice per visit. A completed visit that already carries an invoice cannot acquire a second one, so a re-delivered completion event — or a double-tapped button in the field — yields the first invoice again rather than raising another.

Which clients are on this path, and the three-state setting that decides it, is in can it handle residential and commercial clients. The link such an invoice carries has to outlive the send, which constrains which link it can be — see a payment link you can store.

Gate 1 — the duplicate-invoice guard (leg 1)

Runs at propose time. It refuses to stage when another invoice for the same client has an equal total and an overlapping job or service period, inside a window set by finance.invoicing.duplicate_window_days (default 14).

The critical detail is which siblings it considers: only live invoices — draft, sent, or overdue. A paid or voided sibling is invisible to the guard.

That asymmetry is correct, and it is what makes catch-up billing work. A recurring monthly client legitimately produces identical totals month after month. Once last month's invoice is paid, this month's identical invoice stages without complaint. The guard is aimed at the actual failure mode — issuing the same charge twice while the first one is still outstanding — not at "two invoices look alike".

When it does fire, the response names the conflicting invoice. Re-call with override_duplicate_check: true only after confirming the repeat billing is intentional.

Gate 2 — the jurisdiction validator (leg 3)

Runs at approve time, as a hard gate. This is deliberate: the validator inspects the invoice in its final state, after any edits made while it sat staged.

For the AU locale it checks supplier ABN presence, and — once the invoice total crosses the threshold (thresholdCents, 100000 = $1,000.00) — that the invoice identifies the buyer. The ATO's rule is the buyer's identity or ABN, so a named buyer with no ABN (a private homeowner) passes: the identity is the client's legal name, which the invoice prints as Bill To. The gate refuses only when there is neither a recipient ABN nor a buyer name. A business that wants an ABN specifically can switch on finance.ato.require_recipient_abn_above_threshold. Below the threshold a missing recipient ABN is fine and the send proceeds.

Failure is returned as a structured result, not thrown — read ok: false and the code:

  • missing_supplier_abn
  • missing_recipient_abn_above_threshold

On failure the operation is marked validation_failed, invoice.ato_validation_failed is emitted, and nothing is sent.

The recipient ABN resolves in a fixed order: the invoice header's buyer_abn, then the client's ABN. get_invoice exposes the outcome as effective_recipient_abn plus recipient_abn_source ("header" or "client"), so a compliance check needs no PDF render.

Note that missing_supplier_abn is a property of the sending business, not of any one invoice: until the tenant's own ABN is on file, this gate refuses every send they attempt, through either path. It is the first thing to check when sends fail across the board rather than on one document.

Recipient resolution

propose_send_invoice returns a recipients array with email, name, and resolved_from for each address. Resolution prefers contacts flagged receives_invoices and falls back to the client's billing_email.

This array is the exact fan-out leg 3 will email. Reading it is the cheapest available check against a misconfigured contact — and during a data migration it is often the first place a placeholder billing address surfaces.

Expiry

Pending operations expire via finance.sends.pending_operation_ttl_minutes (default 60, clamped 5–240). An expired operation dispatched nothing and mutated nothing.

What a successful approve returns

{
  "invoice_number": "INV-...",
  "sent_at": "...",
  "message_id": "...",
  "send_ok": true,
  "send_error": null,
  "recipients": ["..."],
  "validation": { "ok": true, "jurisdiction": "AU", "thresholdCents": 100000 }
}

send_ok: true with a message_id is the assertion worth checking in a batch run. validation.ok alone means the invoice was allowed to send, not that it did.

If the client holds available credit at send time, the response also carries credits_on_account_before_send and a credit_hint. Credits auto-apply asynchronously on the invoice.sent event, so the send response cannot report the result — re-read with get_invoice to see whether the invoice became partially_paid or paid.

Related

Under the hood

Source files this page is checked against:

  • lib/operations/send-chain.ts
  • lib/finance/assert-invoice-sendable.ts
  • lib/finance/tax-validators.ts
  • lib/finance/duplicate-invoice-guard.ts
  • lib/mcp/tools/finance/approve-send-invoice.ts

Raw markdown: /docs-md/reference/invoice-send-gates-and-guards

Last updated 2026-10-29