Advanced statement search
The regular statement (GET /v1/accounts/{accountId}/statement) filters by
period, operation type and order. Those three the banking provider knows how to
do, which is why they are cheap.
Questions like “which credits landed on this PIX key”, “what came from
this tax ID” or “which entries were above R$ 5,000” the provider cannot
answer. GET /v1/accounts/{accountId}/statement/advanced can — by walking the
statement for the period and evaluating it row by row.
Every call walks the statement for the period, page by page, because the provider does not filter by these fields. A single search may cost dozens of upstream requests and take considerably longer than the regular statement.
It exists to make one-off queries, investigation and manual reconciliation easy. It must not be abused and must never be used for recurring queries or polling.
- To track incoming payments in real time, use webhooks
(
pix.in.completed). The event arrives on its own, immediately, with no scan cost. - For bulk data or closing routines, use the regular paginated statement or the CSV export.
- Always prefer the regular statement when its native filters are enough. Reach for this endpoint only when you need a field that only it can filter.
Recurring use is detectable and will be treated as abuse, the same way abusive DICT lookups are.
The PIX key now appears in the regular statement
Before reaching for advanced search, note that every statement row now carries
the key used in the transaction, in the pixKey field — here and in the
regular statement alike:
What the field means follows the direction of the entry:
Do not confuse it with counterParty.pixKey, which describes the counterparty.
On a credit the provider usually fills pixKey and leaves the counterparty’s
key empty.
If all you need is which key each credit landed on, the regular statement
already answers, and far more cheaply: paginate as usual and read the field.
The CSV/PDF/XLSX export (POST /v1/accounts/{accountId}/exports) also carries
pixKey on every row. Advanced search is only necessary when you need to
filter by it.
Available filters
All are optional and combine with AND (every condition must hold).
Two notes that avoid surprises:
operation is the cheapest filter. It is the only one the provider can
apply on its side, so passing it makes the scan start already narrowed. When you
know the type, pass it.
Name search is forgiving. It ignores accents and case, and does not require
order: each whitespace-separated term must appear somewhere in the name, in any
order. costa miranda and miranda costa both find “Manaire da Costa Miranda”;
jose finds “José”.
Cursor pagination
There is no “page 2”. An entry’s position in the filtered result has no fixed relationship to its position in the raw statement, so jumping to an arbitrary page would mean walking the period from the start all over again.
Each call spends a bounded budget — a maximum number of provider pages and a
wall-clock limit. When the budget runs out before the end of the period, the
response carries exhausted: false and a nextCursor:
The response:
Repeat passing cursor while exhausted is false. When it comes back true,
the period was fully walked and you have everything that matches.
By default, results come oldest first
Without order, the scan runs in ascending date order and the response echoes
order: "asc" to make that explicit.
This is what makes cursor resumption exact, not a presentation preference. The cursor is a position inside the provider’s statement. In descending order, an entry arriving mid-scan enters at the top and pushes every later position one place along — the continuation call would point at an item you already received, and you would get a duplicate row. In ascending order, new entries land at the end and nothing already scanned moves.
Since the default window is the current day, it almost always includes “now” — so this is the common case, not the exception.
I only want the most recent ones
An account with thousands of entries a day, and you need the end of the period, not the start. Two paths, cheapest first:
1. If the regular statement’s filters are enough, do not use this endpoint.
GET /v1/accounts/{accountId}/statement already returns newest first by
default (order=desc) and accepts size up to 500: the first page is the end
of the day, at the cost of one call.
2. If you need the advanced filters (PIX key, counterparty, amount), use
order=desc, preferably with occurredAfter marking the instant you care
about:
The scan starts from the most recent entry and stops the moment it crosses
17:00: exhausted: true, without reading the whole morning. That is what
makes the mode cheap — occurredAfter in desc is a boundary, not just a
filter.
In desc there is no cursor (sending one returns 400 invalid_cursor), for
the reason above: a position moves when a new entry lands. Continuation is by
time. When exhausted is false, the response carries nextOccurredBefore, the
instant of the last row examined; repeat the call passing that value as
occurredBefore:
A time boundary does not move when a new entry lands at the top — that is why
it is safe where a cursor is not. The price is small and predictable: since the
bound is inclusive, rows sharing the same second as the boundary may come
back on the next call. Deduplicate by the row id. Exclusive would be worse: it
would silently skip those rows, and a gap is something you cannot detect.
The other cost is scan work: each continuation in desc re-reads from the
newest down to the boundary before moving on, because the provider only
paginates by position. For the end of the day that is a page or two and does
not matter; for walking a whole day backwards it does — use asc with a
cursor there, which resumes exactly where it stopped.
What occurredAfter / occurredBefore do — and do not do
Both narrow by time of day inside the period (startDate/endDate still
apply) and are inclusive. They require an explicit offset (-03:00 or Z):
without it there is no way to tell whether “18:00” is Brasília or UTC, and a
three-hour mistake here goes unnoticed until the reconciliation comes up short.
They only shorten the scan when they are the boundary in the scan
direction: occurredAfter in desc and occurredBefore in asc stop the
read once crossed. In the opposite direction they are just filters — the
provider only accepts dates, so the pages before the instant are read anyway.
occurredAfter in asc, for instance, returns fewer rows but costs the same
pages.
A call may return zero items and a cursor. It means the budget was spent on rows that did not match — not that there is nothing to find. Keep going with the cursor.
The scan field shows what the call cost. Use it to size your usage: a search
that needs many continuations is a sign the period is too wide, or that the
filter should include operation.
The cursor belongs to the search
The cursor carries a fingerprint of the filters. If you change any of them and
reuse the cursor, the API answers 400 invalid_cursor — start over without a
cursor.
This is protection, not pedantry: a cursor applied to a different filter set would point at a position unrelated to the new search, and the result would come back silently wrong.
Errors
In the dashboard
The same search is in the backoffice, under Advanced search, with the filters as a form and a ready-made “receipts by key” preset. The screen shows how many rows were examined and marks the result as partial while the scan is unfinished.