# A trip's budget and what it actually cost

> A trip's budget, what it actually cost, and the rates that convert it.

## A trip's budget, spend and rates all live on the trip document

There is no separate costs endpoint in v2 — `costs`, and the currencies a trip's money moves in, are sections of the trip itself (see /skill/add-a-trip.md):

```http
PATCH /api/v2/{user}/trips/{trip}
Content-Type: application/json
Authorization: Bearer fs_agent_…

{"costs": {"budget": {"total": 12000, "days": 45, "currency": "CHF"}, "items": [{"label": "Rail pass", "amount": 420, "category": "preparation"}], "note": "The rail pass is the decision everything else follows from."}}
```

`budget.total` and, optionally, `budget.days` and `budget.currency` (absent means the journal's base currency); `items` are preparation costs — money spent *before* leaving — in the same shape as a day's own `costs` lines; `note` is `costs.md`'s old prose body. What was spent *on* the trip belongs on its days, not here — see /skill/add-a-day.md. Either kind of cost line may carry `stop`, a `plan.route[].id` — see /skill/add-a-trip.md — when the spend belongs to one particular stop rather than the trip as a whole.

A write takes at most 200 cost lines on one day (or in `items`), each `label` at most 200 characters — `limits.costLinesMax` and `limits.costLabelMaxChars` at `GET /api/v2/status`. Past either, the write is refused with `400 invalid_request` naming the field.

`rates.currencies` is the list of currencies this trip's figures may name; `rates.manual` supplies or overrides a rate the server's own reference table (ECB) does not carry. **The convention is units per 1 EUR** — `{"VND": 30500}` — which is a different convention from v1's "per 1 unit of the journal's base currency"; read the trip back and check the number rather than assuming the old shape.

Either section may be declined instead — `declined: {"costs": "we didn't track spending on this trip"}` — and a trip that says nothing about either is refused with `422 incomplete`.

## What the trip actually cost, from a bank statement

```http
POST /api/v2/{user}/media
{"intent": {"kind": "bank_export", "trip": "japan-2027", "declined": {"format": "let the server detect it"}}, …}
GET  /api/v2/{user}/statements/{src}
```

`intent.format` is asked or declined like everything else at this door (see /skill/ingest-photos.md) — name which importer reads the export, or decline it and let the server try to detect one.

Staging a statement through the media door answers with its `src`; `GET .../statements/{src}` reads it as a report — merchants, payments and the rate each foreign currency actually cost, taken from the money the bank moved. **It writes nothing.** Agree the categories with the person, merchant by merchant — a statement says what was paid, never what it was for, and `other` is a real answer.

```http
POST /api/v2/{user}/trips/{trip}/costs/apply
Content-Type: application/json

{"rows": [{"date": "2026-06-22", "label": "Padaria Central", "amount": 11.65, "currency": "CHF", "category": "food"}]}
```

Writes the agreed rows onto the days they belong to. A date whose day nobody has written yet is never silently attached to a neighbour: its rows are filed onto the trip's own `costs.items` instead, and the answer names how many dates that happened for.

The same bounds hold here: a `label` longer than 200 characters is cut to that length, and rows that would put more than 200 cost lines on one day (or on the trip's own `items`) are refused with `400 invalid_costs`. Nothing is written, and the answer says which date it was.
