Skip to content
Last updated

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:

EndpointRole
GET /account/balance?date=Opening and closing balance
GET /cash-in/summaryDeposits that settled
GET /cash-out/summaryPayments that settled
GET /cash-in · GET /cash-outLine-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.

# 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

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"
{
  "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.

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.

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:

"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 sendIt means
from=2026-08-012026-08-01T00:00:00-03:00
to=2026-08-012026-08-01T23:59:59.999-03:00
from=2026-08-01T09:00:002026-08-01T09:00:00-03:00
from=2026-08-01T12:00:00Z2026-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, with a machine-readable code.

CodeStatusCause
LIST_FROM_INVALID422from missing or not a valid date
LIST_TO_INVALID422to missing or not a valid date
LIST_RANGE_INVALID422to is not after from
LIST_FROM_BEFORE_CUTOFF422Statement only — from is earlier than the first available statement date (the detail states it)
LIST_LIMIT_INVALID422limit outside 1–1000
LIST_CURSOR_INVALID422cursor was altered or truncated
BALANCE_DATE_OUT_OF_RANGE422Date in the future, or before 2026-04-01
BALANCE_HISTORY_UNAVAILABLE422Ledger gap for an account on that date
BALANCE_PROVIDER_UNAVAILABLE503Live 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.