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

ParameterTypeRequiredDescription
firstIntNoNumber of records to return. Default: 1000. Maximum: 5000.
afterStringNoOpaque 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

  1. Send your initial query with first and a filter.
  2. Check pageInfo.hasNextPage. If true, more results are available.
  3. Send the same query with the after argument set to pageInfo.endCursor.
  4. Repeat until hasNextPage is false.

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',''))")
done

Python

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_EXPIRED error.
  • If a cursor expires mid-pagination, restart from the first page by omitting the after argument.
  • An INVALID_CURSOR error 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 fieldTypeDescription
transactionIdStringExact match on transaction ID. Pinpoint filter.
trackingIdStringExact match on tracking ID. Pinpoint filter.
personIdStringExact match on person ID. Pinpoint filter.
shopIdStringFilter by shop ID.
createdDateFromString (date)Start of the date range (inclusive). Format: yyyy-MM-dd.
createdDateToString (date)End of the date range (inclusive). Format: yyyy-MM-dd.
statusStringFilter by transaction status (e.g., APPROVED, CANCELLED).
amountFromString (money)Minimum amount (inclusive).
amountToString (money)Maximum amount (inclusive).
paymentMethodNameStringFilter by payment method name (e.g., PIX).

Payout filters

Filter fieldTypeDescription
transactionIdStringExact match on transaction ID. Pinpoint filter.
trackingIdStringExact match on tracking ID. Pinpoint filter.
personIdStringExact match on person ID. Pinpoint filter.
shopIdStringFilter by shop ID.
createdDateFromString (date)Start of the date range (inclusive).
createdDateToString (date)End of the date range (inclusive).
statusStringFilter by transaction status.
amountFromString (money)Minimum amount.
amountToString (money)Maximum amount.
settlementAmountFromString (money)Minimum settlement amount.
settlementAmountToString (money)Maximum settlement amount.
editedDateFromString (date)Filter by last-edited date (start).
editedDateToString (date)Filter by last-edited date (end).

Claim filters

Filter fieldTypeDescription
claimIdStringExact match on claim ID. Pinpoint filter.
transactionIdStringExact match on transaction ID. Pinpoint filter.
shopIdStringFilter by shop ID.
createdDateFromString (date)Start of the date range (inclusive).
createdDateToString (date)End of the date range (inclusive).
statusStringFilter by claim status.
claimReasonStringFilter by claim reason.
processedDateFromString (date)Filter by processed date (start).
processedDateToString (date)Filter by processed date (end).
refundedAmountFromString (money)Minimum refunded amount.
refundedAmountToString (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 createdDateFrom to createdDateTo exceeds 90 days, you receive a DATE_WINDOW_EXCEEDED error.
  • If you omit date filters, the API defaults to the last 90 days.
  • If createdDateFrom is after createdDateTo, you receive an INVALID_DATE_RANGE error.

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 first greater than 5000, you receive a PAGE_SIZE_EXCEEDED error.
  • If you omit first, the default page size is 1000.
  • The summary.pageSizeLimit field 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 first value (up to 5000) to reduce the number of pages.
  • Process pages promptly without long pauses between requests.