Intake and the posting gate
This is the money path’s HTTP surface, and it contains two gates approved by two different people.
The posting checker may not be any maker, including the person who approved the intake.
All routes are prefixed
/intake, require a verified identity, and are org-scoped.
Mutating routes take their acting identity from the derived actor; see Auth, actors and org scoping.
Reads
GET /intake/queue
The review inbox.
counts is what the review page’s tab badges render, so the tabs cannot disagree with the rows.
GET /intake/{intake_id}
One row in full, or 404.
A review deep link resolves through this route.
The UI’s
/books/review?item=<id> searches the whole queue rather than the selected tab, and confirms an id the loaded list does not hold with a direct call here.
An id the backend answers 403 or 404 for selects nothing and shows a by-id banner - it never falls back to a neighbouring document that has live Approve and Reject controls.GET /intake/{intake_id}/audit
The maker-checker trail for one document.
posted event’s detail carries the whole accounting fact: the journal entry id, the obligation id the posting opened, the VAT register line id, and party_resolution - whether the obligation’s party key is a resolved master id or the document’s own free-text name.
GET /intake/{intake_id}/posting
The posting gate as a review pane should render it: the row’s own truth, plus the paused proposal when the agent runtime is reachable.
makers is who may not release this gate, returned so the UI can explain a 409 before it happens rather than as a failed request.
posted_by is read from the journal entry, never from approved_by.
Those are two different people under strict separation of duties, and reporting the intake approver as the posting checker would misstate who signed for the money.
This route is read-only and side-effect free: opening a document must never launch or post anything.
When the agent runtime is unreachable, gate.available is false and the row’s own status still tells the truth.
The intake gate
POST /intake/{intake_id}/approve
No body.
Advances the row to ACTIVE and launches the Bookkeeper.
Separation of duties is checked here as well as at the posting gate, so a document cannot enter ACTIVE in a state from which it could never be posted.
Failing at the gate the human is standing at beats failing two gates later.
POST /intake/{intake_id}/reject
POST /intake/{intake_id}/resubmit
Sends a document back for correction with a patch of fields.
400.
The posting gate
POST /intake/{intake_id}/post-approve
No body.
The checker is the derived actor.
Releases the pause, and the Bookkeeper posts exactly one balanced immutable entry through the single writer, flipping the row to posted in the same transaction.
Idempotent.
A row already
posted returns its entry with no second run.
POST /intake/{intake_id}/post-reject
The launch outbox
An approval and the agent launch it triggers are two things, and the second can be interrupted. Rather than losing it, the launch is recorded as a job.POST /intake/launches/sweep
The reaper.
Retries every launch whose backoff is due - the cron and operations route.
abandoned jobs are excluded by design: a dead letter needs a deliberate retry, not a sweep.
POST /intake/{intake_id}/retry-launch
An operator retry for one row: an interrupted, deferred, failed or dead-lettered launch.
launched is an idempotent no-op, reported as {"idempotent": true}.
Both routes are attributable like every other mutating route, because rearming a dead-lettered document is a decision someone owns.
Where it is enforced
The schema-resilience wrapper that returns
200-empty on a missing legacy table is attached to the read routers only, deliberately.
Mounted app-wide it also covered /post-approve, /post-reject and /posting, turning schema drift on a money route into an HTTP 200 with an empty body.
The posting routes fail closed with a real error.The test that would fail if it broke
langgraph_chat/devcenter/tests/test_intake_api.py covers the route contract and every error mapping.
test_duplicate_double_post.py covers the double-post guard, and test_posting_attribution.py covers posted_by being read from the entry rather than from approved_by.
Related
- The posting gate guide - the same flow from a reviewer’s seat
- The intake gate guide - the first gate, walked through
- Maker-checker - the rule these routes enforce
- The posting mandate - what makes
post-approveunforgeable