Search, Exports and Pagination
Filter, sort and page through any collection, export the results to a CSV or Excel file, and read list operations page by page.
Every collection you can query in bulk has a search operation (POST .../search) that takes the same request body, and most have an export that writes the same results to a file. A few collections also have a list operation (GET) that pages with a cursor. This page covers all three.
Before you start
You need a token. Search operations need only read access. See Environments and Authentication.
Search operations
| Collection | Search | Export to a file |
|---|---|---|
| Batches | Search Batches | Export Batches |
| Payments | Search Payments | Export Payments, then Get Payment Export |
| Payees | Search Payees | Export Payees |
| Payment requests | Search Payment Requests | — |
| Wallets | Search Wallets | — |
| Wallet entries | Search Wallet Entries | — |
| Wallet disputes | Search Wallet Disputes | — |
| Buyers | Search Buyers | Export Buyers |
| Checkouts | Search Checkouts | Export Checkouts |
| Refunds | Search Refunds | Export Refunds |
| Subscriptions | Search Subscriptions | Export Subscriptions |
| Subscription plans | Search Subscription Plans | Export Subscription Plans |
| Plan offers | Search Plan Offers | Export Plan Offers |
| Terminals | Search Terminals | — |
| Settlements | Search Settlements | Export Settlements |
| Settlement items | Search Settlement Items | Export Settlement Items |
Each search operation accepts filters and sorts only on a fixed list of fields. The list is in the operation's description on its reference page.
The request body
{
"filter": {
"all": [
{ "fact": "batchId", "operator": "equals", "value": "B-01JBX7R2M4Q8T6N3K5W9Y1Z0AC" },
{ "fact": "status", "operator": "equals", "value": ["FAILED", "RETURNED"] }
]
},
"sort": [{ "fieldName": "auditInfo.createdAt", "order": "desc" }],
"page": 1,
"pageSize": 100
}| Field | Required | Description |
|---|---|---|
filter | No | Which records to return. Omit it to return every record. |
sort | No | Sort keys, applied in order. Each is `{ "fieldName": "...", "order": "asc" |
page | No | Page number, starting at 1. Default 1. |
pageSize | No | Records per page, from 10 to 1000. Default 100. |
Any other top-level field is rejected with 400 VALIDATION_ERROR.
Filters
A filter is a group: all (every condition must match, AND) or any (at least one must match, OR). A group holds conditions, other groups, or both, nested as deep as you need.
{
"filter": {
"all": [
{ "fact": "taxJurisdiction", "operator": "equals", "value": "us" },
{
"any": [
{ "fact": "status", "operator": "equals", "value": "FAILED" },
{ "fact": "payee.payable", "operator": "equals", "value": false }
]
}
]
}
}Rules:
- One group per level:
{ "all": [...], "any": [...] }at the same level is rejected. Nest one inside the other instead. - A group cannot be empty.
- Each condition has
fact(the field),operator, andvalue(exceptblankandnotBlank). - When the field holds a list (for example
tags.key), a condition matches if any item in the list matches.
Operators
| Operator | Matches when the field | value |
|---|---|---|
equals | equals the value. Send an array to match any of several values. | Value or array of values |
notEqual | does not equal the value. Note the spelling: notEqual, not notEquals. | Value |
contains | contains the text, ignoring case. | Text |
notContains | does not contain the text, ignoring case. | Text |
startsWith | starts with the text, ignoring case. | Text |
endsWith | ends with the text, ignoring case. | Text |
blank | is missing or empty. | None |
notBlank | has a value. | None |
greaterThan, greaterThanOrEqual | is greater than (or equal to) the value. | Number or ISO 8601 date |
lessThan, lessThanOrEqual | is less than (or equal to) the value. | Number or ISO 8601 date |
inRange | is between the two values, both included. | Two-item array [from, to] |
Tips:
- For
equals, send the value exactly as the API returns it, for example the statusCOMPLETED, notcompleted. - Filter dates with ISO 8601 UTC strings:
{ "fact": "auditInfo.createdAt", "operator": "inRange", "value": ["2026-10-01T00:00:00.000Z", "2026-10-31T23:59:59.999Z"] }. - Filter amounts on
wholeValue, the amount in minor units (cents for USD), where the operation offers it. See IDs, Amounts and Dates.
Sorting
Always send sort when you read more than one page. Without it, the order is not guaranteed, so records can move between pages. Add a field that is unique per record (for example id on payments and batches) as the last sort key to break ties, when the operation lets you sort on one.
The response
{
"page": 1,
"pageSize": 100,
"count": 237,
"data": [
{ "id": "P-01JBX7S9QK3M8T2N6R4W0Y5ZDE", "status": "FAILED" }
]
}count: how many records match the filter, across all pages.data: the records on this page. Each has the same shape as the single-object read for that collection.- There are more pages while
page × pageSize < count.
Reading every page
page × pageSize cannot exceed 10,000. A request past that returns 400 VALIDATION_ERROR with the rule code INVALID_PAGE_TOO_HIGH. To read more than 10,000 records, split the work into date windows (for example one month of auditInfo.createdAt per pass) and page through each window.
The helper below pages through one search. Use sort fields that the operation accepts; auditInfo.createdAt and id work for payments and batches.
type SearchPage<T> = { page: number; pageSize: number; count: number; data: T[] };
async function searchAll<T>(token: string, path: string, filter: unknown): Promise<T[]> {
const pageSize = 1000;
const results: T[] = [];
for (let page = 1; (page - 1) * pageSize < 10_000; page++) {
const response = await fetch(`https://api-sandbox.paymentlabs.io${path}`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
filter,
sort: [{ fieldName: "auditInfo.createdAt", order: "asc" }, { fieldName: "id", order: "asc" }],
page,
pageSize,
}),
});
if (!response.ok) throw new Error(`Search failed with ${response.status}: ${await response.text()}`);
const body = (await response.json()) as SearchPage<T>;
results.push(...body.data);
if (page * pageSize >= body.count) break;
}
return results;
}curl -s -X POST https://api-sandbox.paymentlabs.io/payments/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filter": { "all": [ { "fact": "referenceId", "operator": "equals", "value": "invoice-7781" } ] },
"sort": [ { "fieldName": "auditInfo.createdAt", "order": "desc" } ],
"pageSize": 10
}'Export results to a file
An export runs the same filter and sort as the matching search and writes every match to one CSV or Excel file. It takes no page or pageSize. Use it for reports and reconciliation instead of paging through a large result.
| Query parameter | Description |
|---|---|
fileType | csv (default) or xlsx. |
timezoneOffset | Your offset from UTC in minutes, as JavaScript's Date.getTimezoneOffset() returns it (300 for UTC−5). Dates in the file use that time zone. From -840 to 720; default 0. |
Most exports return 200 with { "fileUrl": "..." }. The link expires 60 seconds after the response, so download the file straight away and call the export again for a new link.
Export Payments is the exception, because payment exports can be large. It returns 201 with an exportId and builds the file in the background. Poll Get Payment Export every few seconds until status is COMPLETED, then download fileUrl. FAILED carries an errorMessage; start a new export. An export can be read for 2 days.
curl -s -X POST "https://api-sandbox.paymentlabs.io/checkouts/search/export?fileType=csv&timezoneOffset=300" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "filter": { "all": [ { "fact": "status", "operator": "equals", "value": "PAID" } ] } }'Export errors are the search errors below plus 400 INVALID_FILE_TYPE and 400 INVALID_TIMEZONE_OFFSET.
Ledger accounts have their own date-range exports; see Funding and Ledger Accounts.
Freshness
Search results can lag a few seconds behind the latest changes. Treat search as a way to find records, and read the single object (for example Get Payment) before you act on a status or a balance.
List operations with a cursor
Some collections are read with GET and a cursor instead of search:
| Operation | Cursor parameter | Page size parameter |
|---|---|---|
| List Batch Payments | exclusiveStartId | — |
| Get Batch Imports | exclusiveStartId | scanLimit |
| Get Import Rows | exclusiveStartId | scanLimit |
| List Webhook Logs | exclusiveStartId | limit |
They return:
{
"items": [],
"currentCount": 0,
"totalCount": 12,
"nextPage": "P-01JBX7S9QK3M8T2N6R4W0Y5ZDE"
}- Call without
exclusiveStartId. - If the response has
nextPage, call again withexclusiveStartIdset to it. - Stop when
nextPageis absent.
A page can hold fewer items than you asked for, even zero, while more remain. Do not stop on a short page and do not use totalCount to decide when to stop; follow nextPage only.
Other list operations, such as List Payee Payments, return every item in one array and take no paging parameters.
Errors and edge cases
| Status | code | Meaning | What to do |
|---|---|---|---|
400 | VALIDATION_ERROR | The body is invalid. The rule code in details.filter, details.sort..., details.page or details.pageSize says why. | Fix the body. |
400 | INVALID_REQUEST | The search could not run. | Retry once. If it fails again, simplify the filter and contact support with the request. |
422 | INVALID_BODY | The body is not valid JSON. | Send a JSON object. |
Rule codes you may see inside details:
Rule code | Meaning |
|---|---|
INVALID_FILTER_ONLY_ALL_OR_ANY | filter has a key other than all or any. |
INVALID_FILTER_ALL_AND_ANY | A group uses all and any at the same level. |
INVALID_FILTER_ALL_CRITERIA, INVALID_FILTER_ANY_CRITERIA | A group is empty. |
INVALID_FIELD_FILTER | An item in a group is neither a condition nor a group. |
INVALID_FIELD_NOT_ALLOWED | fact or fieldName is not a searchable field for this operation. |
INVALID_OPERATOR_REQUIRED, INVALID_OPERATOR_OPTION | operator is missing or is not one of the operators above. |
INVALID_PAGE_TOO_HIGH | page × pageSize is above 10,000. |
INVALID_FIELD_TOO_LOW, INVALID_FIELD_TOO_HIGH | page is below 1, or pageSize is outside 10 to 1000. |
Next steps
Updated 4 days ago
What’s Next
Next: Payouts Overview.
