Batch writes and batch entry
Record many expenses, checks, bills, invoices, journal entries or bank-line codings in one call, or type many expenses, checks, bills or invoices into one grid in the app. Every item is governed, checked and audited exactly as if it were sent on its own. (Dated 2026-10-18; ADR-0201.)
The endpoint: POST /v1/batches
curl -X POST https://api.evenbead.com/v1/batches \
-H "Authorization: Bearer $EVENBEAD_KEY" -H "X-Agent-Model: your-model@version" \
-H "Idempotency-Key: oct-close-batch-0001" -H "Content-Type: application/json" \
-d '{
"mode": "per_item",
"items": [
{ "type": "expense", "idempotency_key": "oct-fuel-0003",
"body": { "payment_account": "2100", "doc_date": "2026-10-03", "memo": "Fuel", "lines": [{ "account": "6400", "amount_cents": 5210 }] } },
{ "type": "check", "body": { "payment_account": "1000", "doc_date": "2026-10-01", "number": "1043", "lines": [{ "account": "6700", "amount_cents": 180000 }] } },
{ "type": "bank_line_coding", "bank_line_id": "6f0c…", "body": { "account": "6300" } }
]
}'
| Field | Meaning |
|---|---|
items | 1 to 50 items, run in order. |
items[].type | expense, check, bill, invoice, journal_entry or bank_line_coding: the single operations createExpense, createCheck, createBill, createInvoice, proposeJournalEntry and categorizeBankLine. |
items[].body | That operation's request body, exactly as it takes it, without dry_run. |
items[].bank_line_id | bank_line_coding only: the line (the single operation's path id). |
items[].idempotency_key | Optional. The item's own key; omitted, it is <batch Idempotency-Key>.<index>. |
mode | per_item (default) or atomic. |
dry_run | true previews every item; nothing is written. |
The answer (200) lists every item in order: status (created, proposed, refused; on a dry run would_create, would_propose, refused), the single operation's http_status and response body (result), replayed, and for a refusal the operation's own error. outcome is completed, partial, refused or previewed; counts adds them up.
The rules
- Same governance as one by one. Each item runs through its own operation: its validation, scope, the posting policy in the database (your threshold, confidence floor, first-time vendors, sensitive accounts, period locks, an AI app's account scope and daily cap). An agent's expenses, checks, bills, invoices and journal entries are proposals a person approves; a bank coding posts only when a fact the database checks backs it (a person's rule, a vendor history the company allows) within your limit. There is no bulk bypass.
- Aggregate caps count every item. A connected AI app's daily cap sees the batch's earlier items: the item that would cross it waits for a person (
over_daily_cap). - Partial success. In
per_itemmode a refused item writes nothing and the others are kept. Inatomicmode one refusal refuses the batch: 422batch_refused, every item's verdict inerror.details.items, nothing written. - Approvals are never batch items. Approve proposals with the review queue's operations (people decide).
- Idempotency, twice. The batch's
Idempotency-Keyreplays the whole answer (the same key with other items is 409). Each item's key is the single operation's key: resending an item, in another batch or alone, replays it instead of recording it again; the same key with a different body refuses that item withidempotency_conflict. A refused item stores nothing, so fix it and resend it under its key. - Rate limits. A batch counts as one request per item against the key's and the company's per-minute limits (60, 300 or 1,200 by plan). A batch larger than the key's per-minute budget is refused with
batch_refusedandmax_items. - Audit. Each item's events carry
meta.batch(id,index); abatch.itemevent records each verdict and what it made, and abatch.completedevent the counts.
The MCP tool: create_batch
The same operation as a tool. Arguments: items (each {type, body, idempotency_key?, bank_line_id?}; body is the single tool's arguments without idempotency_key and dry_run), mode, dry_run, and the batch's idempotency_key. Errors come back as tool results with isError: true and the API's error body.
Batch entry in the app
Open Expenses, Bills or Invoices, then More, Enter many at once (or go to /app/batch). Pick the kind on the tabs, then type one per row: date, payee, vendor or customer (suggestions as you type), number, due date, account, amount and memo. Empty rows are skipped; Add 10 rows goes up to 50.
- Review checks every row against your books without saving: each row says "Ready to save", "It will wait for approval", or what to fix.
- Save records the good rows. Saved rows leave the grid and are listed with links; refused rows stay where you typed them, with the reason. Saving the same grid twice never records a row twice.
- Keyboard. Tab moves across a row, Enter moves down a column, and a new row's date starts as the row above's. The total follows the amounts.
- On a phone each row is a card with its labels.