# Invoice send: gates and guards

> 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.

URL: https://biloh.com.au/docs/reference/invoice-send-gates-and-guards
Category: Reference | Audience: builder | Updated: 2026-10-29

**At a glance**

- **Who it's for:** Engineers building on Biloh
- **What it does:** [Invoices out, money in](https://biloh.com.au/capabilities/invoicing-and-payments) · [A finished job that bills itself](https://biloh.com.au/capabilities/instant-invoicing)
- **Tools it uses:** [`propose_send_invoice`](https://biloh.com.au/connect#propose_send_invoice) · [`confirm_pending_operation`](https://biloh.com.au/connect#confirm_pending_operation) · [`approve_send_invoice`](https://biloh.com.au/connect#approve_send_invoice) · [`propose_resend_invoice`](https://biloh.com.au/connect#propose_resend_invoice) · [`get_invoice`](https://biloh.com.au/connect#get_invoice)
- **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:

| Leg | Call | What it does | What it does **not** do |
|---|---|---|---|
| 1 | `propose_send_invoice` | Creates a pending operation, resolves recipients, renders the PDF and email preview, runs the duplicate guard | Send anything. Change the invoice |
| 2 | `confirm_pending_operation` | Moves the operation to `staged`, awaiting operator approval | Send anything |
| 3 | `approve_send_invoice` | Runs 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](/docs/how-to/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 chain | Send on completion |
|---|---|---|
| Pending operation | Yes, legs 1–3 | None |
| Who approves | The operator, at leg 3 | Nobody; the client's setting is the standing approval |
| Jurisdiction validator | Runs, hard | Runs, hard — the same gate |
| Duplicate-invoice guard | Runs at propose time | Not on this path; see below |
| Recipient resolution | `receives_invoices` contacts, then `billing_email` | Identical |
| `invoice.sent` | Emitted | Emitted |

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](/docs/explanation/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](/docs/explanation/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

```json
{
  "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

- [Personalise a staged send](/docs/how-to/personalise-a-staged-send)
- [A payment link you can store](/docs/explanation/a-payment-link-you-can-store)
- [Record a cash payment taken on site](/docs/how-to/record-a-cash-payment-taken-on-site)
