---
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, company settings (modules, automation, account settings and reconciliation rules, routines, contacts, intercompany links, chart mapping, account templates), 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 **next** environment:

| | |
|---|---|
| API base | `https://api.next.accountflow.com/v1` |
| Token endpoint | `https://auth.next.accountflow.com/realms/accountflow/protocol/openid-connect/token` |
| Application (create API clients here) | `https://next.accountflow.com` |
| Developer portal and live spec | `https://developer.next.accountflow.com` · `https://developer.next.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`. Keys are scoped per API client and per operation, and a
   stored response is replayed for 24 hours - after that a retry executes again. This
   is what makes retrying after a timeout safe.
4. **Lists are cursor-paginated**: `{ "data": [...], "pagination": { "nextCursor",
   "hasMore" } }`. Loop until `nextCursor` is null. Cursors are opaque and bound to
   the exact filters; never construct or modify one. `pageSize` is 1-200 (default 50);
   anything outside that range is `400 validation_failed`, not clamped.
5. **One casing rule.** JSON fields, query parameters and path parameters are
   `camelCase` (`pageSize`, `reconciliationStatus`, `{companyId}`); `error` codes and
   problem `reason` values are `snake_case`; scopes are `resource:action`; headers
   are `Http-Case`. Actions are `POST …/actions/<verb>`; deletes take no body
   (`DELETE /companies/{companyId}?reason=` for the optional audit note).
6. **Money is decimal.** Parse amounts as decimals, never floats. When sending, use
   decimal strings (`"-1250.00"`, at most four decimals) - that is how the spec
   documents import amounts; numbers are accepted too.
7. **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`. `errors[]` items are always `{field, message, index?,
   allowedValues?}`. Do not parse `title` or `detail`. `400 validation_failed` is a
   malformed request (fix the syntax); `422 unprocessable` is a well-formed request a
   rule refused (fix the data).
8. **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.
9. **Long work is `202` + poll.** The body is the resource to poll (for imports, the
   import at `GET /companies/{companyId}/imports/{importId}`); stop on a terminal
   status. Its `jobId` is also pollable at `GET /jobs/{jobId}`, `GET /jobs` lists the
   jobs this client submitted, and `job.succeeded` / `job.failed` webhooks announce
   the same thing.
10. **Every response carries `X-Request-Id`** (problem documents repeat it as
    `requestId`). Log it on every call, and send your own to correlate. Requests
    larger than 8 MiB are refused with `413`.
11. **`/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 (following
`pagination.nextCursor`). 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 `:`, `/`, `{`, `}` or whitespace).
  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: [{field, message, index}]`
  (index is 0-based into the request's array). Surface them to the user per line. A
  year whose ledger belongs to another accounting system is `409
  company_not_api_managed` even when the company is bound to Api. An import that
  ends `failed` with `failureReason` `internal_error` or `job_expired` is safe to
  re-send with a new key.
- 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. The
reconciliation summary's `approvalStatus` is `APPROVED`, `REVOKED` or null. Comments,
flags and pending accrual suggestions are read-only (`ledger:read`:
`…/comments`, `…/flags`, `…/accrual-suggestions`); the API cannot write them. Access
reads (`users:read`) include `GET /organizations/{organizationId}/users/{userId}/effective-access`;
policy rules carry `scopeType` (`COMPANY`, `ORGANIZATION`, `PORTFOLIO`) and only the
first two can be written through the API.

## Company settings

Settings are read with `settings:read` and changed with `settings:write`; neither is in
the default grant. An API client acts for its whole organization, so `settings:write`
lets it change any company's settings there — including `autoApprove` and
`autoPostOnHighConfidence`, which let Accountflow approve or post without a person.
Have a person confirm such a change before you make it.

- **Every change is a patch.** A field you leave out stays as it is. To clear a value or
  hand it back to the default, name it in `reset` (where the operation has one).
- **Name the version you read.** Settings reads return a `version` (company settings,
  the contact list, the template choice, an intercompany link or the groups, each
  routine source), `companyRoutineVersion` (an account's own routine) or
  `settingsRevision` (an account). Send it back as `expectedVersion` or
  `settingsRevision`. `409 conflict` with a `currentVersion` means the settings changed
  since you read them: read again, re-apply your change to the fresh values, and send
  that. Never just retry with the new version.
- **Deactivated, not deleted:** contacts and intercompany links. A deactivated link
  keeps its accounts, so those accounts cannot go on another link until they are taken
  off it; the `409` (`reason: intercompany_…`) names the link (`definitionId`, `active`).
- **Chart mapping** only to standard accounts of the company's own country
  (`GET …/standard-accounts`). `POST …/account-mappings` maps up to 500 rows in one
  call: a row that cannot be mapped comes back `failed` with a `message` and the rest
  are mapped, so read every row's `status`.
- **Applying an account template overwrites values the company changed itself.**
  `GET …/account-template/preview` first and show the user the changes and `warnings`;
  then `POST …/account-template/actions/apply` with the preview's `entryRevisions` and
  only the warnings the user confirmed (`confirmedWarnings`). `409` means the template
  changed since the preview (preview again); `422` with `reason: confirmation_required`
  lists the `warnings` still unconfirmed — ask the user, never confirm for them.

## Webhooks

Thin events: the payload names what changed (`id`, `type`, `occurredAt`,
`data.{resourceType, resourceId, organizationId}`), then you fetch it. Verify the
signature on the **raw** body before parsing, answer the verification handshake with
the hex `HMAC-SHA256(secret, challenge)` (echoing the challenge is not enough),
deduplicate on the event id (delivery is at-least-once), and respond fast - do the
work asynchronously. Re-verification runs only when the endpoint's `url` changes;
deleting an endpoint cancels anything still queued for it. 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 `nextCursor` is null; `pageSize` within 1-200
- [ ] 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
- [ ] settings writes are patches naming the read's `expectedVersion` / `settingsRevision`;
      a version `409` re-reads and re-applies, never blindly retries
- [ ] automatic approval or posting, and an account template apply, confirmed by a person
      first; `confirmedWarnings` holds only what they confirmed
- [ ] errors surfaced with `error`, `reason` and `requestId`; `X-Request-Id` logged
- [ ] tests cover a 404, a 409, a 422 with `errors[]`, a 429 with `Retry-After`, and a
      replayed idempotent response
