Skip to main content
JTL’s APIs return large collections in pages rather than all at once. Any endpoint that returns a list of items (products, invoices, customers, orders, etc.) includes pagination metadata so you can navigate through the results. The JTL Platform uses two pagination styles depending on the API surface:
APIPagination styleHow it works
REST API (JTL-Wawi, SCX)Page-basedPass pageNumber and pageSize as query parameters
GraphQL API (JTL-Wawi)Cursor-basedPass first (page size) and after (cursor) as query variables

Page-based Pagination (REST)

The REST API uses page-based pagination. Each response includes the current page of items plus metadata telling you the total number of items, how many pages exist, and whether there are more pages to fetch.

Making a Paginated Request

curl -X GET "https://api.jtl-cloud.com/erp/v2/invoices?pageNumber=1&pageSize=20" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-Tenant-ID: $TENANT_ID"

Response Structure

Every paginated REST response follows this structure:
{
  "totalItems": 256,
  "pageNumber": 1,
  "pageSize": 20,
  "totalPages": 13,
  "hasPreviousPage": false,
  "hasNextPage": true,
  "nextPageNumber": 2,
  "previousPageNumber": null,
  "items": [
    { ... },
    { ... }
  ]
}

Metadata Fields

FieldTypeDescription
totalItemsnumberTotal items across all pages
pageNumbernumberCurrent page
pageSizenumberNumber of items per page
totalPagesnumberTotal number of pages
hasNextPagebooleanWhether more pages exist after this one
hasPreviousPagebooleanWhether pages exist before this one
nextPageNumbernumber | nullThe next page number, or null if this is the last page
previousPageNumbernumber | nullThe previous page number, or null if this is the first page
itemsarrayItems for this page

Requesting a Specific Page

Pass pagination parameters in the query string:
GET https://api.jtl-cloud.com/erp/v2/invoices?pageNumber=2&pageSize=20
ParameterTypeDescription
pageNumbernumberThe page number (starts at 1)
pageSizenumberItems per page

Fetching the Next Page

Use hasNextPage and nextPageNumber to determine if there are more results:
const response = await fetch(
  "https://api.jtl-cloud.com/erp/v2/invoices?pageNumber=1&pageSize=20",
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
      "X-Tenant-ID": tenantId,
    },
  }
);
 
const data = await response.json();
 
console.log(`Page ${data.pageNumber} of ${data.totalPages}`);
console.log(`Showing ${data.items.length} of ${data.totalItems} items`);
 
if (data.hasNextPage) {
  console.log(`Next page: ${data.nextPageNumber}`);
  // Fetch the next page using data.nextPageNumber
}

Fetching All Pages

To retrieve every item across all pages, loop until hasNextPage is false:
async function fetchAllItems<T>(
  baseUrl: string,
  headers: Record<string, string>,
  pageSize = 20
): Promise<T[]> {
  const allItems: T[] = [];
  let pageNumber = 1;
  let hasNextPage = true;
 
  while (hasNextPage) {
    const url = `${baseUrl}?pageNumber=${pageNumber}&pageSize=${pageSize}`;
    const response = await fetch(url, { headers });
 
    if (!response.ok) {
      throw new Error(`API error (${response.status}) on page ${pageNumber}`);
    }
 
    const data = await response.json();
    allItems.push(...data.items);
 
    hasNextPage = data.hasNextPage;
    pageNumber = data.nextPageNumber ?? pageNumber + 1;
 
    console.log(
      `Fetched page ${data.pageNumber} of ${data.totalPages} (${allItems.length}/${data.totalItems} items)`
    );
  }
 
  return allItems;
}
 
// Usage
const allInvoices = await fetchAllItems(
  "https://api.jtl-cloud.com/erp/v2/invoices",
  {
    Authorization: `Bearer ${accessToken}`,
    "X-Tenant-ID": tenantId,
  }
);

Cursor-based Pagination (GraphQL)

The GraphQL API uses cursor-based pagination. Instead of page numbers, you use an opaque cursor string to request the next set of results. This is more reliable for large, frequently changing datasets because new or deleted records don’t shift the page boundaries.

Making a Paginated Request

Pass first (page size) and optionally after (cursor) as query variables:
const query = `
  query GetERPItems($first: Int, $after: String) {
    QueryItems(first: $first, after: $after) {
      nodes {
        id
        sku
        name
      }
      pageInfo {
        hasNextPage
        endCursor
      }
      totalCount
    }
  }
`;
 
// First page
const variables = { first: 20 };
 
// Subsequent pages: include the cursor from the previous response
// const variables = { first: 20, after: "eyJza2lwIjoyMH0=" };
What this does: Fetches the first 20 items. The pageInfo object tells you whether more pages exist and provides the cursor for the next page.

Response Structure

{
  "data": {
    "QueryItems": {
      "nodes": [
        {
          "id": "895d2d4d-4ed4-44c7-ac61-ebec01000000",
          "sku": "AR2016041-VKO",
          "name": "Men's T-shirt"
        }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "endCursor": "eyJza2lwIjoyMH0="
      },
      "totalCount": 674
    }
  }
}

Metadata Fields

FieldTypeDescription
nodesarrayItems for this page
pageInfo.hasNextPagebooleanWhether more results exist beyond this page
pageInfo.endCursorstringOpaque cursor to pass as after for the next page
totalCountnumberTotal number of matching items across all pages

Fetching the Next Page

Pass the endCursor from the previous response as the after variable:
let hasNextPage = true;
let cursor: string | null = null;
 
while (hasNextPage) {
  const variables: Record<string, unknown> = { first: 20 };
  if (cursor) {
    variables.after = cursor;
  }
 
  const response = await fetch("https://api.jtl-cloud.com/erp/v2/graphql", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${accessToken}`,
      "X-Tenant-Id": tenantId,
    },
    body: JSON.stringify({
      operationName: "GetERPItems",
      query: ITEMS_QUERY,
      variables,
    }),
  });
 
  const { data } = await response.json();
  const page = data.QueryItems;
 
  console.log(
    `Fetched ${page.nodes.length} items (${page.totalCount} total)`
  );
 
  // Process page.nodes here
 
  hasNextPage = page.pageInfo.hasNextPage;
  cursor = page.pageInfo.endCursor;
}
What this does: Loops through pages by passing the endCursor from each response as the after variable in the next request. Stops when hasNextPage is false.

Best Practices

Use the smallest page size you need. Use smaller page sizes for UI-driven fetching (10-20 items). Use larger sizes only for background sync or batch jobs (50-100 items). Don’t rely on total counts for logic. Both totalItems (REST) and totalCount (GraphQL) can change between requests as data is created or deleted. Use them for display only, not for control flow. Handle empty pages gracefully. If a page has no items (e.g., records were deleted between requests), the items array will be empty. Don’t treat this as an error:
if (data.items.length === 0 && !data.hasNextPage) {
  // No more items
  break;
}
Respect rate limits when fetching all pages. Large paginated fetches can hit rate limits. Add a delay or use exponential backoff between requests.
// Add a delay between page requests to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 200));
Avoid parallel page fetches. Fetch pages sequentially unless you control rate limits and ordering. Parallel fetches can cause ordering issues and increase the risk of rate limiting. Choose the right pagination style for your use case. If you’re calling REST endpoints (invoices, payments), use page-based. If you’re querying ERP data through GraphQL (items, categories, customers), use cursor-based. Don’t mix them up.

Quick Reference

QuestionREST (page-based)GraphQL (cursor-based)
How do I set page size?pageSize query parameterfirst variable
How do I get the next page?Use nextPageNumberUse endCursor as after
How do I know when I’m done?hasNextPage is falsepageInfo.hasNextPage is false
What’s the total count?totalItemstotalCount
Can I jump to a specific page?Yes (pageNumber=5)No (traverse from the start)

Next Steps

Rate Limiting

Understand request quotas, especially important when fetching many pages.

Error Handling

Handle errors that may occur during paginated fetches.

Using Platform APIs

Full guide to querying the GraphQL API with filtering, sorting, and pagination.

Webhooks

Instead of polling pages for changes, use webhooks to get notified in real time.