# Categorize transactions

Recommended agent workflow using stable category codes, counterparties, and evidence.

Always discover the chart before proposing changes. Prefer categoryCode (e.g. 5300) over free-text labels. Include confidence, reason, and structured evidence for human review. Keep list batches small (limit≤50).

> **Ontology.** See Reference → Ontology for chart codes, synonyms, and seed counterparties (AWS → 5300). Public JSON-LD: GET /api/v1/ontology.jsonld

## Workflow

1. GET /categories — load codes, names, synonyms, parentCode
2. GET /transactions?status=uncategorized&limit=50 — find work items
3. Match description to counterparties (AWS, Figma, …) or category synonyms
4. GET /transactions/:id — read notes and receipts when unsure
5. POST /proposals/categorization with categoryCode, confidence, reason, evidence
6. GET /proposals — verify proposals are pending
7. Tell the user: pending in Ledger → Approvals; durable trail in Log (/log)

### `GET /api/v1/categories`

Chart of accounts with stable codes, synonyms, and parent links. Call before proposing categorizations. Prefer categoryCode over display names.

- operationId: `list_categories`
- scope: `read`

**Response**

```json
{
  "data": [
    {
      "code": "5100",
      "name": "Software & tools",
      "type": "expense",
      "parentCode": "5000",
      "synonyms": ["Software", "SaaS", "Subscriptions"],
      "inUse": true
    }
  ]
}
```

### `GET /api/v1/transactions`

List ledger transactions for the token's organization. Results are ordered by date descending. Agents should keep limit≤50 unless exporting.

- operationId: `list_transactions`
- scope: `read`

**Parameters**

- `status` (enum, optional) — uncategorized, matched, receipt_missing, agent_suggested, reconciled
- `source` (enum, optional) — nordea, stripe, manual_csv
- `date` (date, optional) — Exact date (YYYY-MM-DD)
- `dateFrom` (date, optional) — Inclusive start date
- `dateTo` (date, optional) — Inclusive end date
- `category` (string, optional) — Exact category name, or __uncategorized__
- `description` (string, optional) — Case-insensitive substring on description
- `limit` (integer, optional) — Default 50 (agent-lean), max 500
- `offset` (integer, optional) — Default 0

**Response**

```json
{
  "data": [ { "id": "…", "date": "2026-08-28", "amount": -842, "currency": "EUR", … } ],
  "meta": { "totalCount": 42, "limit": 50, "offset": 0 }
}
```

### `POST /api/v1/proposals/categorization`

Suggest a category for a transaction. Prefer categoryCode. Creates a pending proposal — the ledger is not updated until a human approves in Ledger → Approvals.

- operationId: `propose_categorization`
- scope: `propose-write`

**Body**

```json
{
  "transactionId": "uuid",
  "categoryCode": "5300",
  "confidence": 0.91,
  "reason": "Description matches counterparty AWS",
  "evidence": [
    { "type": "counterparty_match", "value": "aws" }
  ]
}
```

**Response**

```json
{ "data": { "id": "…", "status": "pending", "type": "categorization", … } }
```

```json Example proposal body
{
  "transactionId": "550e8400-e29b-41d4-a716-446655440000",
  "categoryCode": "5300",
  "confidence": 0.91,
  "reason": "Description matches counterparty AWS",
  "evidence": [
    { "type": "counterparty_match", "value": "aws" }
  ]
}
```

> On approval, the transaction receives the canonical category name, status matched, and your proposedBy, confidence, and reason metadata. The propose and approve/reject events both appear in Log.
