# Conventions & errors

## Field conventions

- `snake_case` names; dates are ISO-8601 (`YYYY-MM-DD`), timestamps RFC 3339.
- A field with no value is an explicit `null` — keys are never omitted.
- Transaction ids are random strings (other resources use integer ids).
  Always use ids exactly as the API returned them.
- Amounts are flat numeric fields next to a `currency` field, like a bank
  statement. `*_eur` fields (`amount_eur`, `gross_eur`, `net_eur`) carry the
  EUR equivalent — use those when summing across currencies.

## Split transactions

Only **top-level transactions** (real bank movements) appear in lists. A split
transaction (`is_split: true`) carries its allocation pieces in `children`,
each with its own amount and category. The children always sum exactly to the
parent amount, so:

> **For cashflow totals, sum parent amounts only.** Never add children on top —
> that double-counts.

Requesting a child's id on `GET /transactions/{id}` returns its **parent**
(with the child nested) — payment records on expenses reference child pieces,
and following that reference lands on the real bank movement.

## Pagination

List endpoints return:

```json
{ "data": [ … ], "next_cursor": "84123" }
```

To get the next page, pass `next_cursor` back as `?cursor=…` exactly as
received. `null` means you have everything. Check `next_cursor`, not the page
size: a page can contain fewer than `limit` rows while more pages exist.

## Rate limits

**60 requests/minute per token**, refilled continuously at 1/second. Every
response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and
`X-RateLimit-Reset`; a `429` adds `Retry-After` (seconds). Honor it and retry.

## Errors

Every non-2xx response has the same body:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "date_from must be a valid date in YYYY-MM-DD format, got \"junk\"",
    "request_id": "req_1f2e3d4c5b6a79880911"
  }
}
```

Branch on `code` (stable contract); `message` text may change. Quote
`request_id` (also on the `X-Request-Id` header) when contacting support.

| Status | Code | Meaning |
|---|---|---|
| 401 | `missing_token` | No/malformed `Authorization` header |
| 401 | `invalid_token` | Token unknown |
| 401 | `token_revoked` | Token was revoked in Settings |
| 401 | `token_expired` | Token past its expiry date |
| 400 | `invalid_company_key` | `X-Company-Key` missing or not the token's company |
| 400 | `invalid_request` | Invalid parameter — the message names the field |
| 404 | `not_found` | No such resource in your company |
| 429 | `rate_limited` | Bucket empty — wait `Retry-After` seconds |
| 500 | `internal_error` | Our fault — retry later, quote `request_id` to support |

Invalid parameters always fail with `400` — filters are **never silently
ignored**, so you can't accidentally act on wrong data.
