Reading data
What accounts.read lets you call, how money is represented, and what a failed sync tells you.
La documentation partenaires est publiée en anglais.
#The surface
accounts.read permits:
| Call | Returns |
|---|---|
GET /api/accounts | the user's accounts with balances |
GET /api/accounts/{id}/transactions?from&to&limit&offset | a page of the statement, newest first |
GET /api/connections | the bank connections behind the accounts |
POST /api/accounts/{id}/sync | refresh one account from the bank; body { "uid": "…" } |
POST /api/sync | refresh every account; body { "items": [{ "accountId": "…", "uid": "…" }] } |
Everything else answers 403; a revoked or expired key answers 401. Sensitive fields — including the bank-side uid the sync calls need — are encrypted envelopes, so use an SDK: it decrypts locally and posts the uid for you.
#Money
Inside the envelopes — what the SDKs return — amounts are decimal strings ("1234.56"), never numbers, so no float rounding can creep in. The plaintext amount field on the raw wire is not the value; only the decrypted one is. The sign lives in creditDebitIndicator (CRDT or DBIT), not in the amount; apply it at the presentation edge. Balances carry their ISO 20022 type code (ITBD booked, ITAV available, …).
#Connected but empty
A fresh bank connection is zero-knowledge: the server lists the accounts and fetches balances once at onboarding, but it cannot read the identifier it would need to pull transactions — only a client holding the user's key can. So for every user with a registered key (every Connect user), call syncAll() (or POST /api/accounts/{id}/sync) yourself after connecting; transactions do not arrive on their own, and polling without a sync of your own polls forever. Your first read can legitimately return zero accounts while the bank session settles — render "syncing…" and retry for a bounded time (the reference implementation uses 90 seconds), then sync.
#Sync failures
A failed POST /api/accounts/{id}/sync reports the class of failure in the status and a stable reason in a ProblemDetails body, so you branch without parsing prose. POST /api/sync answers 200 and reports the same vocabulary per account in failures[] — except consent_withdrawn: the bulk call leaves withdrawn accounts out silently, so only the per-account call ever answers it.
| Status | reason | What it means | What to do |
|---|---|---|---|
| 409 | reconnect_needed | the consent needs the user again | send them through a reconnect |
| 409 | consent_withdrawn | terminal for this account | remove the connection and authorise afresh |
| 429 | rate_limited | the bank is throttling | back off hard; honour Retry-After |
| 502 | bank_error | the bank answered, and answered no | do not hammer it |
| 503 | transient | something retrying may clear | retry with backoff |
| 503 | partner_app_inactive | your Enable Banking application is not activated | nothing the user can do; activate it |
Branch on reason, never on the status — two reasons share 409. Retrying only helps for 429 and 503.
bankErrorCode is present only when the failure originated at the bank and the value it sent is shaped like a code (for example EXPIRED_SESSION); treat it as detail for logs, never as the field you branch on.
#Two kinds of 429
A 429 with a body (reason: "rate_limited") is the bank throttling and carries a deliberately long Retry-After; Enable Banking suspends an application that over-runs, so do not retry sooner. A 429 with no body is the per-IP quota in the reference — slow your loop down.
#Webhooks
There are none yet. Poll GET /api/accounts after a connect and after a sync; keep the interval kind (a page view's worth, not a tight loop).