---
name: accountflow-api
description: Build and debug integrations against the Accountflow public API (Bridge) - OAuth2 client-credentials auth, reading general ledger, accounts, trial balances, VAT, bank and documents, importing a company's books (accounts, dimensions, sub-ledgers, opening balances, general-ledger lines, open items), correcting or deleting what was imported, async jobs and webhooks. Use whenever code calls an api.*.accountflow.com host, or the user mentions Accountflow, Bridge, or the Accountflow API.
---

# Accountflow public API

You are helping someone build an integration against Accountflow's public API. This
skill was generated for the **lab** environment:

| | |
|---|---|
| API base | `https://api.lab.accountflow.com/v1` |
| Token endpoint | `https://auth.lab.accountflow.com/realms/accountflow/protocol/openid-connect/token` |
| Application (create API clients here) | `https://lab.accountflow.com` |
| Developer portal and live spec | `https://developer.lab.accountflow.com` · `https://developer.lab.accountflow.com/openapi.yaml` |

## Work from the spec, never from memory

`references/openapi.yaml` is the released contract: every path, parameter, schema,
scope and status code. **Before writing a call, find its operation there** (search by
`operationId` or path) and take field names and types from the schema. Do not invent
endpoints, fields or query parameters; if the spec does not have it, the API does not
either. The other files in `references/` are the prose guides:

| Need | Read |
|---|---|
| first call, client setup, scopes | `references/quickstart.md` |
| pagination, idempotency, reach, rate limits, versioning | `references/conventions.md` |
| pushing a company's books | `references/imports.md` |
| fixing or deleting pushed data, the 409 in-use rule | `references/corrections.md` |
| every error code and what to do about it | `references/errors.md` |
| webhook events, handshake, signature verification | `references/webhooks.md` |

## Rules that hold everywhere (the ones integrations get wrong)

1. **Auth is OAuth2 client credentials.** Exchange `client_id` + `client_secret` for a
   bearer token; cache it until shortly before `expires_in`; never mint one per request.
   What the token may do comes from the scopes granted to the client in Accountflow,
   not from the token request. Secrets come from the user's secret store or environment,
   never hard-coded, never logged.
2. **Outside your reach is `404`, never `403`.** A 404 means "does not exist for this
   client": wrong id, wrong organization, or deleted. Do not treat it as a permission
   error and do not retry it.
3. **Every mutation needs an `Idempotency-Key` header** (a fresh UUID per logical
   action, reused on retries of that same action). Same key + same body replays the
   stored response (`Idempotency-Replayed: true`); same key + different body is `422
   idempotency_key_reused`. This is what makes retrying after a timeout safe.
4. **Lists are cursor-paginated**: `{ "data": [...], "pagination": { "next_cursor",
   "has_more" } }`. Loop until `next_cursor` is null. Cursors are opaque and bound to
   the exact filters; never construct or modify one. `page_size` is 1-200.
5. **Money is decimal.** Parse amounts as decimals, never floats. When sending, prefer
   decimal strings (`"-1250.00"`), at most four decimals.
6. **Errors are RFC 9457 problem documents** (`application/problem+json`). Branch on
   the machine-readable `error` code, and on `reason` when present; show `detail` to
   humans; log `requestId`. Do not parse `title` or `detail`.
7. **Retry only what is retryable**: `429` and `503` (honour `Retry-After`), and `409
   idempotency_in_flight`. Network timeouts on a mutation: retry with the **same**
   `Idempotency-Key`. Everything else in 4xx is a bug in the request - fix it, do not
   retry it.
8. **Long work is `202` + poll.** The body is the resource to poll; stop on a terminal
   status. `job.succeeded` / `job.failed` webhooks announce the same thing.
9. **`/v1` is additive-only.** Ignore unknown response fields; never fail on them.

## A client worth building

Whatever the language, give the integration one small client with: a token cache; a
single request function that sets `Authorization`, sets `Idempotency-Key` on
non-GET calls, parses problem documents into a typed error carrying `status`, `error`,
`reason`, `requestId` and the raw body; the retry policy from rule 7 with capped
exponential backoff; and a pagination helper that yields items across pages. Then
write the feature code on top of that, not around it.

```python
# Shape, not a library - adapt to the project's HTTP client and conventions.
def request(method, path, *, json=None, idempotency_key=None):
    headers = {"Authorization": f"Bearer {tokens.get()}"}
    if method != "GET":
        headers["Idempotency-Key"] = idempotency_key or str(uuid.uuid4())
    for attempt in range(5):
        r = http.request(method, BASE + path, json=json, headers=headers, timeout=30)
        if r.status_code in (429, 503) or (r.status_code == 409 and error_code(r) == "idempotency_in_flight"):
            time.sleep(float(r.headers.get("Retry-After", 2 ** attempt)))
            continue                      # same Idempotency-Key on every attempt
        if r.status_code >= 400:
            raise ApiError.from_problem(r)  # status, error, reason, requestId, body
        return r.json() if r.content else None
    raise ApiError.exhausted()
```

## Importing a company's books

Read `references/imports.md` first. The essentials:

- The company must be bound to the **Api** accounting system in Accountflow, and the
  client needs `imports:write`. `GET /companies/{id}/imports/readiness` tells you where
  the company stands (`blockers`, `nextStep`). `409 company_not_api_managed` means the
  binding is missing - that is a setup step for a human, not something to retry.
- **Order: accounts → dimensions / sub-ledgers (optional) → opening balances →
  general-ledger lines → open items.** Lines naming an account code missing from that
  year's chart are refused (`422`, `reason: unknown_account_codes`).
- Each request holds **at most 10 000 lines** (`413` beyond). Batch larger data and
  send the batches **sequentially**: one import per kind and company runs at a time
  (`409`, `reason: job_in_flight`, with the `jobId` to wait for).
- Each request answers `202` with an import. **Poll `GET
  /companies/{id}/imports/{importId}`** until `succeeded` or `failed`. `succeeded` can
  still carry `rowsFailed > 0`: read `sampleErrors` (`line N: message`, N from 1), fix
  those lines and re-send them.
- **Lines carry the customer's own stable `id`** (no `:`). Re-sending an `id` updates
  that line in place with history kept; a new `id` is a new line. Choose ids that are
  stable in the source system (document number + line number), never random per run.
  Reads return it as `sourceLineId`.
- **`period` (1-12) and `year` are supplied explicitly**, never derived from dates.
- Structural validation failures are `422` with `errors: [{index, field, message}]`
  (index is 0-based into the request's array). Surface them to the user per line.
- Importing triggers nothing else (no reconciliation, no VAT run).

## Correcting and deleting

Read `references/corrections.md`. To change values, **re-send the same `id`**. To
remove, use the deletion endpoints (`…/imports/general-ledger-lines/deletions`,
`…/imports/opening-balances/deletions`, `DELETE …/accounts/{accountNumber}`,
`…/dimensions/{key}`, `…/sub-ledgers/{key}`). Deletion is **all or nothing**, and
anything that work in Accountflow depends on is **never deleted**: the answer is `409`
with a `reason` (`lines_in_use`, `account_in_use`, …) and an `inUse` list of ids and
dependencies. Do not loop on that 409. Report the `inUse` list to the user; the ways
out are removing the dependent work in Accountflow, posting a reversing line, or (for
accounts) importing with `hidden: true`. Ids the ledger does not have come back under
`result.unknown` on the import - not an error, so deletions are safe to retry.

## Reading data

Reads need the matching read scope (`ledger:read`, `vat:read`, `bank:read`,
`documents:read`, …). Accounts are identified by account number + year; general-ledger
lines by `lineId` (Accountflow's) and, for imported lines, `sourceLineId` (yours).
`year` omitted means the company's current accounting year. Start from
`GET /v1/whoami` and `GET /v1/companies` to discover what the client can reach.

## Webhooks

Thin events: the payload names what changed, then you fetch it. Verify the signature on
the **raw** body before parsing, answer the verification handshake, deduplicate on the
event id (delivery is at-least-once), and respond fast - do the work asynchronously.
Details and the exact signature scheme are in `references/webhooks.md`.

## Before you call the integration done

- [ ] every endpoint, field and status code used exists in `references/openapi.yaml`
- [ ] secrets from environment or secret store; tokens cached; nothing sensitive logged
- [ ] `Idempotency-Key` on every mutation, reused across retries of the same action
- [ ] retries only for 429 / 503 / `idempotency_in_flight` / timeouts, with backoff
- [ ] pagination loops until `next_cursor` is null
- [ ] amounts handled as decimals end to end
- [ ] imports: batched to 10 000, sequential per kind, polled to a terminal status,
      `rowsFailed` and `sampleErrors` surfaced, stable ids
- [ ] deletions: `409 *_in_use` reported to the user with the `inUse` list, not retried
- [ ] errors surfaced with `error`, `reason` and `requestId`
- [ ] tests cover a 404, a 409, a 422 with `errors[]`, a 429 with `Retry-After`, and a
      replayed idempotent response
