# Daily Reconciliation This guide shows how to close a day against ConnectPSP: prove that what you started with, plus what moved, equals what you ended with. ``` opening balance + cash-in − cash-out = closing balance ``` Four endpoints do all the work: | Endpoint | Role | | --- | --- | | `GET /account/balance?date=` | Opening and closing balance | | `GET /cash-in/summary` | Deposits that settled | | `GET /cash-out/summary` | Payments that settled | | `GET /cash-in` · `GET /cash-out` | Line-by-line detail when a figure does not match | ## 1. Get both ends of the day `?date=D` returns the **closing** balance of day `D`. So the opening balance of `D` is simply the closing balance of the day before. ```bash # Opening balance of 2026-08-01 curl "https://api.homologacao.connectpsp.com/account/balance?date=2026-07-31" \ -H "Authorization: Bearer $TOKEN" \ -H "ApplicationToken: $APP_TOKEN" # Closing balance of 2026-08-01 curl "https://api.homologacao.connectpsp.com/account/balance?date=2026-08-01" \ -H "Authorization: Bearer $TOKEN" \ -H "ApplicationToken: $APP_TOKEN" ``` This reads the banking ledger, not the bank's live API, so it keeps answering even when a partner bank is unreachable. Two limits worth knowing: - History starts at **2026-04-01**. Earlier dates return `422` with code `BALANCE_DATE_OUT_OF_RANGE`. - If the ledger has a gap for one of your accounts on that date, you get `422` with code `BALANCE_HISTORY_UNAVAILABLE` instead of a number. This is deliberate — a stale balance presented as a closing balance is worse than no answer. Omit `date` entirely to get the live balance right now. ## 2. Get the movement ```bash curl "https://api.homologacao.connectpsp.com/cash-in/summary?from=2026-08-01&to=2026-08-01" \ -H "Authorization: Bearer $TOKEN" \ -H "ApplicationToken: $APP_TOKEN" ``` ```json { "from": "2026-08-01T00:00:00-03:00", "to": "2026-08-01T23:59:59-03:00", "currency": "BRL", "totals": { "paid": { "count": 1284, "amount": 192450.75 }, "pending": { "count": 96, "amount": 14320.00 }, "refunded": { "count": 2, "amount": 300.00 } }, "settlement": [ { "ispb": "12345678", "bank": "SETTLEMENT PARTNER A", "count": 1103, "amount": 165230.75 }, { "ispb": "87654321", "bank": "SETTLEMENT PARTNER B", "count": 181, "amount": 27220.00 } ] } ``` **Only `paid` moves money.** `pending` is what has not settled; it belongs to no balance yet. The `settlement` array counts **paid transactions only**, which is what lets you reconcile bank by bank against the `banks[]` array of `GET /account/balance`. Do the same for `/cash-out/summary`. There the keys are the Cash-Out statuses in lowercase, so what settled is `completed`. Then: ``` closing = opening + cashIn.totals.paid.amount − cashOut.totals.completed.amount − cashIn.totals.refunded.amount + cashOut.totals.refunded.amount ``` ## 3. When a figure does not match, list it The listing endpoints walk the same period transaction by transaction. ```bash curl "https://api.homologacao.connectpsp.com/cash-in?from=2026-08-01&to=2026-08-01&limit=200" \ -H "Authorization: Bearer $TOKEN" \ -H "ApplicationToken: $APP_TOKEN" ``` ### Walking the pages Pagination is **cursor-based**. Each response carries a `nextCursor`; send it back to get the next page. When the response arrives **without** `nextCursor`, the period is exhausted. ```javascript async function fetchAll(resource, from, to) { const items = []; let cursor = null; do { const url = new URL(`https://api.homologacao.connectpsp.com/${resource}`); url.searchParams.set('from', from); url.searchParams.set('to', to); url.searchParams.set('limit', '200'); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${TOKEN}`, ApplicationToken: APP_TOKEN, }, }); if (!res.ok) throw new Error(`${resource} failed: ${res.status}`); const page = await res.json(); items.push(...page.items); cursor = page.nextCursor ?? null; // absent means we are done } while (cursor); return items; } ``` Three rules for the walk: 1. Keep `from`, `to` and `limit` **identical** on every page. Only `cursor` changes. 2. Treat `nextCursor` as opaque. Do not parse it, do not build one — the format may change. 3. Stop when `nextCursor` is absent. Do not stop on a short page. ### Why there is no `total` Returning a total would mean counting the entire period on every request, which is the exact cost that cursor pagination exists to avoid. Because the cursor seeks straight to its position, page 900 costs the same as page 1 — but only if nobody asks for the count. If you need a count, `GET /cash-in/summary` gives it for the whole period in one call. ## Reading a transaction Every item in the list is the same object you get from `GET /cash-in/{id}` or `GET /cash-out/{id}`. Two fields matter most for reconciliation: **`endToEndId`** — the BACEN/SPI settlement ID. This is the common key between your records and ours, and between both of us and the bank. **`settlement`** — the ConnectPSP-side bank and account that settled the transaction: ```json "settlement": { "ispb": "12345678", "bank": "SETTLEMENT PARTNER A", "branch": "0001", "account": "123456" } ``` Do not confuse it with `payer.bankData` / `payee.bankData`, which describe the **counterparty**. `settlement` is our side — it is what maps a transaction onto a line of `GET /account/balance`. ## Timezone `from` and `to` are read in **America/Sao_Paulo (UTC-3)** unless you send an explicit offset. A plain date covers that whole calendar day locally: | You send | It means | | --- | --- | | `from=2026-08-01` | `2026-08-01T00:00:00-03:00` | | `to=2026-08-01` | `2026-08-01T23:59:59.999-03:00` | | `from=2026-08-01T09:00:00` | `2026-08-01T09:00:00-03:00` | | `from=2026-08-01T12:00:00Z` | `2026-08-01T09:00:00-03:00` | Every timestamp in a response comes back the same way — ISO 8601 with a fixed `-03:00` offset. The period filters on when the transaction was **created**, not when it settled. A charge created at 23:58 and paid at 00:03 belongs to the first day. ## Errors All errors follow [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807), with a machine-readable `code`. | Code | Status | Cause | | --- | --- | --- | | `LIST_FROM_INVALID` | 422 | `from` missing or not a valid date | | `LIST_TO_INVALID` | 422 | `to` missing or not a valid date | | `LIST_RANGE_INVALID` | 422 | `to` is not after `from` | | `LIST_FROM_BEFORE_CUTOFF` | 422 | Statement only — `from` is earlier than the first available statement date (the `detail` states it) | | `LIST_LIMIT_INVALID` | 422 | `limit` outside 1–1000 | | `LIST_CURSOR_INVALID` | 422 | `cursor` was altered or truncated | | `BALANCE_DATE_OUT_OF_RANGE` | 422 | Date in the future, or before 2026-04-01 | | `BALANCE_HISTORY_UNAVAILABLE` | 422 | Ledger gap for an account on that date | | `BALANCE_PROVIDER_UNAVAILABLE` | 503 | Live balance only — the bank did not answer. Retry | ## Suggested daily routine 1. `GET /account/balance?date=D-1` → opening. 2. `GET /cash-in/summary` and `GET /cash-out/summary` for `D` → movement. 3. `GET /account/balance?date=D` → closing. 4. Check the identity, per bank, using `settlement[]` against `banks[]`. 5. Only if a bank does not close, walk `GET /cash-in` and `GET /cash-out` for that day and compare by `endToEndId`. Steps 1 to 4 are four requests, regardless of your volume. Step 5 is the expensive one, and you only pay for it when something is actually wrong.