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.

This endpoint is expensive. Read this before integrating.

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:

{
"endToEndId": "E18236120202608081220s15f7488fc7",
"direction": "IN",
"operation": "PIX",
"amount": 1300,
"pixKey": "5bdc1526-20c1-4820-ae06-cf9d6ff5a21c",
"counterParty": { "name": "Manaire da Costa Miranda", "document": "40579906825" }
}

What the field means follows the direction of the entry:

DirectionWhat pixKey means
IN (credit)the key of this account that received the money
OUT (debit)the destination key of the payment

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).

ParameterEffect
directionIN (credits) or OUT (debits)
operationPIX, TED, INTERNAL_TRANSFER, BOLETO, FEE
statusCOMPLETED, PROCESSING, FAILED, REFUNDED, REVERSED
pixKeyexact key, case-insensitive
counterpartyNamepart of the counterparty name
counterpartyDocumentcounterparty CPF/CNPJ, with or without punctuation
minAmount / maxAmountamount range, on the absolute value
startDate / endDateperiod, 31 days maximum
occurredAfter / occurredBeforetime-of-day bounds inside the period, RFC 3339 with offset (2026-09-20T17:00:00-03:00), inclusive on both ends
orderasc (default, oldest to newest, with cursor) or desc (newest to oldest, continuation by time — see below)

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:

# First call
curl -G "https://tenant.api.corpx.com/v1/accounts/$ACCOUNT/statement/advanced" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: $TENANT" \
-d startDate=2026-07-10 -d endDate=2026-08-08 \
-d operation=PIX -d direction=IN \
-d pixKey=5bdc1526-20c1-4820-ae06-cf9d6ff5a21c
# Continuation, while exhausted=false
curl -G "https://tenant.api.corpx.com/v1/accounts/$ACCOUNT/statement/advanced" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: $TENANT" \
-d startDate=2026-07-10 -d endDate=2026-08-08 \
-d operation=PIX -d direction=IN \
-d pixKey=5bdc1526-20c1-4820-ae06-cf9d6ff5a21c \
-d cursor=MTA6NDI6MzE0MTU5

The response:

{
"accountId": "acc_...",
"startDate": "2026-07-10",
"endDate": "2026-08-08",
"items": [ /* statement rows, same shape as the regular statement */ ],
"size": 12,
"order": "asc",
"exhausted": false,
"nextCursor": "MTA6NDI6MzE0MTU5",
"partialReason": "scan_budget",
"scan": { "pagesRead": 10, "itemsScanned": 1000, "maxPages": 10 }
}

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:

# Everything received on this key since 17:00 today, newest first
curl -G "https://tenant.api.corpx.com/v1/accounts/$ACCOUNT/statement/advanced" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Tenant-Id: $TENANT" \
-d order=desc \
-d occurredAfter=2026-09-20T17:00:00-03:00 \
-d direction=IN -d pixKey=5bdc1526-20c1-4820-ae06-cf9d6ff5a21c

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:

{
"items": [ /* 50 rows, newest first */ ],
"size": 50,
"order": "desc",
"exhausted": false,
"partialReason": "page_full",
"nextOccurredBefore": "2026-09-20T17:42:10-03:00",
"scan": { "pagesRead": 2, "itemsScanned": 137, "maxPages": 10 }
}

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.

An empty response with a cursor is not the final answer

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 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

CodeWhen
date_range_too_wideperiod longer than 31 days
invalid_date_rangemalformed dates, or end before start
invalid_filtervalue outside the domain (direction, operation, order), invalid amount, instant without offset, or occurredAfter later than occurredBefore
invalid_cursormalformed cursor, one from a different filter set, or sent with order=desc
partner_rate_limitedthe provider throttled our traffic. If the scan had already collected something, we return the partial result with a cursor instead of an error

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.