Developers

The propose → commit contract

Why no Invoia tool writes on its own, and what the two steps look like.

Invoia moves money and books to an accounting ledger. An agent that misreads "five hours" and silently deletes seven is not a support ticket, it is a wrong VAT return. So the server has no tool that both decides and writes.

Two steps, always

  1. Propose. A plan_* tool reads the current state, works out the change, and returns a plan in plain language plus an opaque proposal_token. Nothing has happened yet.
  2. Commit. The separate commit tool takes that token and applies the plan exactly as written.

The agent is instructed to show you the plan and wait. Because the two steps are different tools, a client that requires approval for writes can gate commit alone and leave reading and drafting free.

What a plan looks like

A ready proposal comes back as text the agent can read to you, with the structured form alongside it:

Set Jane Doe to 12.0 h (was 7.0) and 60% complete on line
"SEO retainer — July" of invoice INV-2026-0142.

To apply, confirm with the user, then call `commit` with
proposal_token="mcpp_3f0c…" (expires 2026-08-07T09:12:33.000Z;
requires mcp:write).

You read the summary. If it is right, the agent calls commit with the token. If it is not, you say so and nothing was ever written.

The token has a short life

  • Fifteen minutes. After that the token is PROPOSAL_EXPIRED and the plan_* tool has to run again. This is deliberate: a plan describes the world as it was when it was built, and an hour-old plan is a guess.
  • Single use. Committing flips the proposal from pending to committed in one conditional write, so a client that retries a timed-out commit cannot apply it twice.
  • Bound to your account. A token from one connection means nothing on another.

PROPOSAL_STALE means the ground moved

Every plan records the updated_at of each row it depends on, and commit re-checks them. If someone edited that invoice in the app while you were reading the plan, the commit is refused rather than applied to a record it no longer describes. Re-run the same plan_* tool, read the new summary, commit that token.

When the agent does not have enough to go on

A plan_* tool can come back with needs_clarification and a list of questions instead of a plan. The agent must ask you and re-call the same tool with your answers — it is not allowed to guess:

I need a bit more information before preparing this change:
1. [line_ref] Which line should the hours go on?
   - Dev hours (hourly, invoice INV-2026-0142)  [answer: line_7]
   - SEO retainer — July (value, invoice INV-2026-0142)  [answer: line_9]

Ask the user, then call this tool again with line_ref filled in.

Each option carries the exact value to send back, so a text-only agent is never stuck able to read a choice but unable to express it.

The most common trigger is a scale ambiguity:

  • hours is an absolute cumulative total, not an increment. Sending hours: 5 to a line that already has 12 logged is a request to remove seven — so the tool stops and asks which you meant. Use hours_delta to add.
  • completion_rate is 01; completion_percent is 0100.
  • money is in major units — kroner, not øre — in the integration's base currency.

Every failure says what to do next

Tool failures come back as readable errors, not transport faults, and each carries a code and a next step:

Error [INVOICE_ALREADY_ISSUED]: This invoice is already issued.
Next: An issued invoice is frozen. Raise a credit note with
plan_create_credit_note instead of editing it.

The codes are the same vocabulary the REST API uses, so an integration that touches both surfaces branches on one set of strings. The ones specific to this contract:

CodeWhat happenedWhat to do
PROPOSAL_EXPIREDOlder than fifteen minutesRe-run the plan_* tool
PROPOSAL_STALEA record it depends on changedRe-run, re-read, re-commit
PROPOSAL_NOT_FOUNDAlready committed, or another account'sRe-run the plan_* tool
AMBIGUOUS_REFERENCEA name matched several rowsPick from the listed candidates

Irreversible steps are still irreversible

Commit is where the change becomes real, and some changes cannot be undone from Invoia:

  • Issuing books the invoice into Dinero and emails your customer. Correcting it means raising a credit note, not editing the invoice.
  • Credit notes are themselves bookings.

Issuing lives behind its own scope, so an agent can be given the run of everything else without it — see Auth and scopes.