Files
accounted/lib/docs/content/cookbook/ingest-bank-transactions.ts
T
Jakob WennbergandClaude Opus 4.8 3c85082f3a fix(docs): replace dead gnubok.app domain with app.gnubok.se across public docs (#683)
gnubok.app has no DNS delegation (the .app TLD returns no NS records for
it), so every https://gnubok.app/... link in the public docs was dead —
including the OAuth connector URL a Claude Connectors Directory reviewer
would paste from the new Connect-with-Claude page (#682), and the docs
links in the gnubok-mcp npm README that ships in the tarball.

app.gnubok.se is the live production domain (the MCP endpoint there
correctly answers 401 unauthenticated) and was already the default
GNUBOK_URL in the npm bridge and the branding-service appUrl fallback.

Mechanical swap: 55 occurrences across 11 files, all https:// URL form
(verified no emails or identifiers). Docs content only — no code paths.
Also resolves the greptile domain-mismatch finding on #682.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:00:27 +02:00

241 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const COOKBOOK_INGEST_BANK_MD = `# Cookbook — ingest and categorise bank transactions
> Push a bank statement file into Accounted, get AI-assisted category suggestions, commit the categorisations, and match payments against open invoices. End-to-end transaction-to-booking pipeline.
This is the operational companion to the [Transactions reference](/docs/api/reference/transactions) and the [Imports reference](/docs/api/reference/imports). Use it for the first integration where transactions enter the system from a bank source.
## What you'll need
- A test API key with \`transactions:write\`, \`transactions:read\`, and \`imports:write\` scopes.
- A bank statement file in one of the supported formats: CSV (SEB / Swedbank / Handelsbanken / Nordea / Danske / ICA / Lendo / Ålandsbanken / SBAB / Marginalen / others auto-detected), CAMT.053 XML, or a plain account-statement CSV with at minimum date + amount + description columns.
- The settlement account for the bank — typically \`'1930'\` for an SEK business account. Check via \`GET /accounts\`.
## 1. Upload the bank file
\`POST /imports/bank\` accepts multipart upload. Format detection is automatic; the response includes the matched parser. The endpoint kicks off an async operation — you'll poll for the result.
\`\`\`bash
curl "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/imports/bank" \\
-H "Authorization: Bearer gnubok_sk_test_..." \\
-H "Idempotency-Key: $(uuidgen)" \\
-F "file=@statement-2026-04.csv" \\
-F 'settlement_account="1930"'
\`\`\`
Response is a 202 with the operation handle:
\`\`\`json
{
"data": {
"operation_id": "op_a8f1...",
"status": "queued",
"poll_url": "/api/v1/operations/op_a8f1...",
"webhook_event": "operation.completed"
},
"meta": { "request_id": "req_...", "api_version": "2026-05-12" }
}
\`\`\`
## 2. Poll until the import completes
Polling is the simplest pattern; subscribe to the \`operation.completed\` event ([cookbook](/docs/api/cookbook/webhooks)) for the push variant. The operation lifecycle is \`queued → running → succeeded | failed | cancelled\`.
\`\`\`bash
curl "https://app.gnubok.se/api/v1/operations/$OPERATION_ID" \\
-H "Authorization: Bearer gnubok_sk_test_..."
\`\`\`
On \`succeeded\`:
\`\`\`json
{
"data": {
"operation_id": "op_a8f1...",
"type": "import.bank",
"status": "succeeded",
"progress": { "current": 187, "total": 187, "phase": "complete" },
"result": {
"rows_inserted": 165,
"rows_skipped_duplicate": 22,
"format_detected": "seb_csv",
"earliest_date": "2026-04-01",
"latest_date": "2026-04-30"
},
"started_at": "2026-05-01T08:00:00Z",
"completed_at": "2026-05-01T08:00:04Z"
}
}
\`\`\`
Note the dedup: rows that match an existing transaction on \`(date, amount, description_hash)\` are skipped, not inserted twice. Re-uploading the same file is safe.
## 3. List uncategorised transactions
After ingest the rows are in \`transactions\` but uncategorised (\`account_number: null\`, \`category: null\`). List them:
\`\`\`bash
curl "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/transactions?status=uncategorized&period=2026-04&limit=50" \\
-H "Authorization: Bearer gnubok_sk_test_..."
\`\`\`
Response (cursor-paginated, oldest-first):
\`\`\`json
{
"data": [
{
"id": "tx_...",
"transaction_date": "2026-04-03",
"description": "SEB CARD - SJ 25-...",
"amount": -487.00,
"currency": "SEK",
"category": null,
"account_number": null,
"vat_treatment": null,
"document_id": null,
"journal_entry_id": null
},
...
],
"meta": { "request_id": "req_...", "next_cursor": "eyJ0cyI6Ij..." }
}
\`\`\`
## 4. Get category suggestions
\`POST /transactions/{id}/suggest-categories\` returns ranked guesses based on the description, counterparty history, and your booking-template library:
\`\`\`bash
curl -X POST "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/transactions/$TX_ID/suggest-categories" \\
-H "Authorization: Bearer gnubok_sk_test_..."
\`\`\`
\`\`\`json
{
"data": {
"suggestions": [
{
"category": "expense_travel",
"account_number": "5800",
"vat_treatment": "standard_25",
"confidence": 0.92,
"reason": "Counterparty 'SJ' matched booking template 'Tågresor' (12 prior matches)"
},
{
"category": "expense_representation",
"account_number": "6071",
"vat_treatment": "standard_25",
"confidence": 0.15,
"reason": "Fallback — SJ has occasionally been booked as kund-representation"
}
]
}
}
\`\`\`
Confidence ≥ 0.85 is generally safe to auto-apply; below that surface to the user.
## 5. Commit the categorisation
\`POST /transactions/{id}/categorize\` stages the booking. Dry-run first to see the verifikation preview:
\`\`\`bash
curl "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/transactions/$TX_ID/categorize?dry_run=true" \\
-H "Authorization: Bearer gnubok_sk_test_..." \\
-H "Idempotency-Key: $(uuidgen)" \\
-H "Content-Type: application/json" \\
-d '{
"category": "expense_travel",
"account_number": "5800",
"vat_treatment": "standard_25"
}'
\`\`\`
Response includes the would-be journal entry lines:
\`\`\`json
{
"data": {
"staged_operation_id": "po_...",
"preview": {
"journal_lines": [
{ "account": "5800", "debit": 389.60, "credit": 0, "label": "Reskostnader" },
{ "account": "2641", "debit": 97.40, "credit": 0, "label": "Ingående moms 25%" },
{ "account": "1930", "debit": 0, "credit": 487.00, "label": "Företagskonto" }
],
"voucher_number_assigned_on_commit": "auto",
"account_deltas": { "5800": -389.60, "2641": -97.40, "1930": +487.00 }
}
}
}
\`\`\`
Drop \`?dry_run=true\` and reuse the same \`Idempotency-Key\` to commit. The response carries the audit block with the now-posted voucher number.
## 6. Batch categorise
For a backlog, use \`POST /transactions/batch-categorize\` (up to 100 transactions per call, dry-runnable, partial-success on commit):
\`\`\`bash
curl "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/transactions/batch-categorize" \\
-H "Authorization: Bearer gnubok_sk_test_..." \\
-H "Idempotency-Key: $(uuidgen)" \\
-H "Content-Type: application/json" \\
-d '{
"items": [
{ "transaction_id": "tx_1", "category": "expense_travel", "account_number": "5800", "vat_treatment": "standard_25" },
{ "transaction_id": "tx_2", "category": "income_services", "account_number": "3001", "vat_treatment": "standard_25" },
...
]
}'
\`\`\`
Response shape — every item has its own \`ok\` flag:
\`\`\`json
{
"data": {
"results": [
{ "ok": true, "request_index": 0, "data": { "voucher_number": "A2026-0042" } },
{ "ok": false, "request_index": 1, "error": { "code": "PERIOD_LOCKED", "message": "Perioden är låst." } }
],
"summary": { "total": 2, "succeeded": 1, "failed": 1 }
}
}
\`\`\`
## 7. Match a payment against an invoice
When a transaction is a customer payment, match it to the open invoice via \`POST /transactions/{id}/match-invoice\` instead of \`categorize\`. The engine posts the payment voucher (debit 1930, credit 1510) AND marks the invoice paid in a single transaction.
\`\`\`bash
curl -X POST "https://app.gnubok.se/api/v1/companies/$COMPANY_ID/transactions/$TX_ID/match-invoice" \\
-H "Authorization: Bearer gnubok_sk_test_..." \\
-H "Idempotency-Key: $(uuidgen)" \\
-H "Content-Type: application/json" \\
-d '{ "invoice_id": "inv_...", "payment_date": "2026-04-15" }'
\`\`\`
For supplier-invoice payments use \`POST /transactions/{id}/match-supplier-invoice\` — same shape, different counterparty side.
## Multicurrency
Bank statements that include non-SEK transactions are imported with the foreign amount preserved in \`amount_foreign\` + \`currency_foreign\`. When you categorise, the engine looks up the Riksbanken FX rate for the transaction date and books the SEK equivalent on the GL side. The FX delta (rate at booking vs rate at month-end revaluation) is later picked up by the currency-revaluation job.
If you import a multi-currency statement, ensure the company has \`base_currency\` set (defaults to SEK) and that the relevant FX rates are available — fetch via \`GET /currency/rate?date=...&from=...&to=...\` or rely on the cached daily snapshot.
## Common pitfalls
- **Re-running the same file is safe; date-only overlap is also safe.** The dedup keys on \`(date, amount, description_hash)\` so partial overlap of two statements doesn't double-import.
- **Settlement account selection matters.** Importing into the wrong settlement account silently breaks bank reconciliation later. \`1930\` (företagskonto) is the default for SEK; a foreign-currency bank account uses its own asset account (e.g. \`1932\` for USD).
- **Cash-method companies and partial payments don't mix.** If \`company_settings.accounting_method = 'cash'\` and you try to match a partial payment, the response is \`VALIDATION_ERROR\` rather than booking accrual entries — cash-method cannot model the per-installment moms event correctly (ML 13 kap 8 §). Either book the partial payment as a separate categorisation or switch to accrual.
- **Batch-categorize is partial-success by default.** If one item hits a locked period, the others still commit. The summary block tells you the totals; check per-item \`ok\` flags.
## Next steps
- **[Set up webhooks](/docs/api/cookbook/webhooks)** — get notified of \`transaction.categorized\` events without polling.
- **[File a VAT declaration](/docs/api/cookbook/file-vat-declaration)** — compute the rutor 05–62 from your now-categorised transactions.
- **[Transactions reference](/docs/api/reference/transactions)** — every parameter, every filter.
- **[Imports reference](/docs/api/reference/imports)** — full bank-file format coverage.
`