Pagination, Filtering, and Sorting
The Merchant Data API uses cursor-based pagination for all list queries (payins, payouts, claims). This guide explains how to paginate through results, apply filters, and understand the response structure.
Cursor-based pagination
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
first | Int | No | Number of records to return. Default: 1000. Maximum: 5000. |
after | String | No | Opaque cursor from a previous response. Pass the endCursor value to fetch the next page. |
Response structure
Every list query returns a connection object with three sections:
{
edges { # Array of results
node { ... } # The entity (Payin, Payout, or Claim)
cursor # Opaque cursor for this record
}
pageInfo {
hasNextPage # true if more results exist
endCursor # Cursor of the last item; pass as "after" for the next page
}
summary {
rowCount # Number of items in this page
pageSizeLimit # Effective page size limit applied
totalCount # Always null for list queries
}
}Fetching the next page
- Send your initial query with
firstand afilter. - Check
pageInfo.hasNextPage. Iftrue, more results are available. - Send the same query with the
afterargument set topageInfo.endCursor. - Repeat until
hasNextPageisfalse.
Complete pagination loop
curl
# First page
CURSOR=""
HAS_NEXT=true
while [ "$HAS_NEXT" = "true" ]; do
if [ -z "$CURSOR" ]; then
AFTER_CLAUSE=""
else
AFTER_CLAUSE=", after: \"$CURSOR\""
fi
RESPONSE=$(curl -s -X POST https://api.payretailers.com/data-api/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Basic $(echo -n 'YOUR_SHOP_ID:YOUR_API_TOKEN' | base64)" \
-d "{\"query\": \"{ payins(first: 100${AFTER_CLAUSE}, filter: { createdDateFrom: \\\"2026-01-01\\\", createdDateTo: \\\"2026-01-31\\\" }) { edges { node { transactionId amount currency transactionStatusName } cursor } pageInfo { hasNextPage endCursor } summary { rowCount } } }\"}")
echo "$RESPONSE" | python3 -m json.tool
HAS_NEXT=$(echo "$RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['payins']['pageInfo']['hasNextPage'])" | tr '[:upper:]' '[:lower:]')
CURSOR=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin)['data']['payins']['pageInfo']; print(d.get('endCursor',''))")
donePython
import requests
from requests.auth import HTTPBasicAuth
url = "https://api.payretailers.com/data-api/graphql"
auth = HTTPBasicAuth("YOUR_SHOP_ID", "YOUR_API_TOKEN")
all_payins = []
cursor = None
has_next_page = True
while has_next_page:
after_clause = f', after: "{cursor}"' if cursor else ""
query = f"""
query {{
payins(
first: 100
{after_clause}
filter: {{
createdDateFrom: "2026-01-01"
createdDateTo: "2026-01-31"
}}
) {{
edges {{
node {{
transactionId
amount
currency
transactionStatusName
}}
}}
pageInfo {{
hasNextPage
endCursor
}}
summary {{
rowCount
}}
}}
}}
"""
response = requests.post(
url,
json={"query": query},
auth=auth,
headers={"Content-Type": "application/json"},
)
result = response.json()
payins_data = result["data"]["payins"]
for edge in payins_data["edges"]:
all_payins.append(edge["node"])
page_info = payins_data["pageInfo"]
has_next_page = page_info["hasNextPage"]
cursor = page_info.get("endCursor")
print(f"Fetched {payins_data['summary']['rowCount']} records. "
f"Total so far: {len(all_payins)}")
print(f"Done. Total payins: {len(all_payins)}")TypeScript
import axios from "axios";
const url = "https://api.payretailers.com/data-api/graphql";
const credentials = Buffer.from("YOUR_SHOP_ID:YOUR_API_TOKEN").toString("base64");
interface PayinNode {
transactionId: string;
amount: string;
currency: string;
transactionStatusName: string;
}
const allPayins: PayinNode[] = [];
let cursor: string | null = null;
let hasNextPage = true;
while (hasNextPage) {
const afterClause = cursor ? `, after: "${cursor}"` : "";
const query = `
query {
payins(
first: 100
${afterClause}
filter: {
createdDateFrom: "2026-01-01"
createdDateTo: "2026-01-31"
}
) {
edges {
node {
transactionId
amount
currency
transactionStatusName
}
}
pageInfo {
hasNextPage
endCursor
}
summary {
rowCount
}
}
}
`;
const response = await axios.post(
url,
{ query },
{
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${credentials}`,
},
}
);
const payinsData = response.data.data.payins;
for (const edge of payinsData.edges) {
allPayins.push(edge.node);
}
hasNextPage = payinsData.pageInfo.hasNextPage;
cursor = payinsData.pageInfo.endCursor;
console.log(
`Fetched ${payinsData.summary.rowCount} records. Total so far: ${allPayins.length}`
);
}
console.log(`Done. Total payins: ${allPayins.length}`);Cursor behaviour and expiry
- Cursors are opaque strings. Do not parse, modify, or construct them manually.
- Cursors expire after 4 hours. If you pass an expired cursor, you receive a
CURSOR_EXPIREDerror. - If a cursor expires mid-pagination, restart from the first page by omitting the
afterargument. - An
INVALID_CURSORerror means the cursor value has been tampered with or is malformed.
Filtering
Each query type accepts a filter argument with different parameters.
Payin filters
| Filter field | Type | Description |
|---|---|---|
transactionId | String | Exact match on transaction ID. Pinpoint filter. |
trackingId | String | Exact match on tracking ID. Pinpoint filter. |
personId | String | Exact match on person ID. Pinpoint filter. |
shopId | String | Filter by shop ID. |
createdDateFrom | String (date) | Start of the date range (inclusive). Format: yyyy-MM-dd. |
createdDateTo | String (date) | End of the date range (inclusive). Format: yyyy-MM-dd. |
status | String | Filter by transaction status (e.g., APPROVED, CANCELLED). |
amountFrom | String (money) | Minimum amount (inclusive). |
amountTo | String (money) | Maximum amount (inclusive). |
paymentMethodName | String | Filter by payment method name (e.g., PIX). |
Payout filters
| Filter field | Type | Description |
|---|---|---|
transactionId | String | Exact match on transaction ID. Pinpoint filter. |
trackingId | String | Exact match on tracking ID. Pinpoint filter. |
personId | String | Exact match on person ID. Pinpoint filter. |
shopId | String | Filter by shop ID. |
createdDateFrom | String (date) | Start of the date range (inclusive). |
createdDateTo | String (date) | End of the date range (inclusive). |
status | String | Filter by transaction status. |
amountFrom | String (money) | Minimum amount. |
amountTo | String (money) | Maximum amount. |
settlementAmountFrom | String (money) | Minimum settlement amount. |
settlementAmountTo | String (money) | Maximum settlement amount. |
editedDateFrom | String (date) | Filter by last-edited date (start). |
editedDateTo | String (date) | Filter by last-edited date (end). |
Claim filters
| Filter field | Type | Description |
|---|---|---|
claimId | String | Exact match on claim ID. Pinpoint filter. |
transactionId | String | Exact match on transaction ID. Pinpoint filter. |
shopId | String | Filter by shop ID. |
createdDateFrom | String (date) | Start of the date range (inclusive). |
createdDateTo | String (date) | End of the date range (inclusive). |
status | String | Filter by claim status. |
claimReason | String | Filter by claim reason. |
processedDateFrom | String (date) | Filter by processed date (start). |
processedDateTo | String (date) | Filter by processed date (end). |
refundedAmountFrom | String (money) | Minimum refunded amount. |
refundedAmountTo | String (money) | Maximum refunded amount. |
Aggregation filters
Aggregation queries accept createdDateFrom and createdDateTo plus the dataset argument (PAYINS, PAYOUTS, or CLAIMS).
Date range governance
- The maximum date window is 90 days. If
createdDateFromtocreatedDateToexceeds 90 days, you receive aDATE_WINDOW_EXCEEDEDerror. - If you omit date filters, the API defaults to the last 90 days.
- If
createdDateFromis aftercreatedDateTo, you receive anINVALID_DATE_RANGEerror.
Pinpoint filters
When you filter by transactionId, trackingId, personId, or claimId, the default date range restriction is not applied. This allows you to look up specific records regardless of when they were created.
query {
payins(
first: 1
filter: {
transactionId: "9c017f16-a41c-7040-8eb6-31030c7fea76"
}
) {
edges {
node {
transactionId
amount
transactionStatusName
createdTs
}
}
summary {
rowCount
}
}
}Example response:
{
"data": {
"payins": {
"edges": [
{
"node": {
"transactionId": "9c017f16-a41c-7040-8eb6-31030c7fea76",
"amount": "55.03",
"transactionStatusName": "CANCELLED",
"createdTs": "2026-01-31T23:59:15.167Z"
}
}
],
"summary": {
"rowCount": 1
}
}
}
}Sorting
Results are returned in descending order by creation timestamp (newest first). The sort order is fixed and cannot be overridden by query parameters.
Edge cases
Empty results
If no records match your filter, the API returns an empty edges array with hasNextPage: false:
{
"data": {
"payins": {
"edges": [],
"pageInfo": {
"hasNextPage": false,
"endCursor": null
},
"summary": {
"rowCount": 0,
"pageSizeLimit": 1000,
"totalCount": null
}
}
}
}totalCount is always null
The summary.totalCount field is always null for list queries. Use the pagination loop (check hasNextPage) to retrieve all matching records.
Page size limits
- If you request
firstgreater than 5000, you receive aPAGE_SIZE_EXCEEDEDerror. - If you omit
first, the default page size is 1000. - The
summary.pageSizeLimitfield shows the effective page size that was applied.
Cursor expiry during pagination
If your pagination takes longer than 4 hours, the cursor will expire. You must restart from the first page. To avoid this:
- Use a larger
firstvalue (up to 5000) to reduce the number of pages. - Process pages promptly without long pauses between requests.
Updated about 4 hours ago