> ## Documentation Index
> Fetch the complete documentation index at: https://developer.jtl-software.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Paginate through large result sets from JTL APIs

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:

| API                          | Pagination style | How it works                                                     |
| ---------------------------- | ---------------- | ---------------------------------------------------------------- |
| **REST API** (JTL-Wawi, SCX) | Page-based       | Pass `pageNumber` and `pageSize` as query parameters             |
| **GraphQL API** (JTL-Wawi)   | Cursor-based     | Pass `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.

<Note>
  Examples below are in TypeScript. The patterns applies to any language with an HTTP client.
</Note>

### Making a Paginated Request

```bash theme={null}
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:

```json theme={null}
{
  "totalItems": 256,
  "pageNumber": 1,
  "pageSize": 20,
  "totalPages": 13,
  "hasPreviousPage": false,
  "hasNextPage": true,
  "nextPageNumber": 2,
  "previousPageNumber": null,
  "items": [
    { ... },
    { ... }
  ]
}
```

### Metadata Fields

| Field                | Type           | Description                                                   |
| -------------------- | -------------- | ------------------------------------------------------------- |
| `totalItems`         | number         | Total items across all pages                                  |
| `pageNumber`         | number         | Current page                                                  |
| `pageSize`           | number         | Number of items per page                                      |
| `totalPages`         | number         | Total number of pages                                         |
| `hasNextPage`        | boolean        | Whether more pages exist after this one                       |
| `hasPreviousPage`    | boolean        | Whether pages exist before this one                           |
| `nextPageNumber`     | number \| null | The next page number, or `null` if this is the last page      |
| `previousPageNumber` | number \| null | The previous page number, or `null` if this is the first page |
| `items`              | array          | Items 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
```

| Parameter    | Type   | Description                   |
| ------------ | ------ | ----------------------------- |
| `pageNumber` | number | The page number (starts at 1) |
| `pageSize`   | number | Items per page                |

### Fetching the Next Page

Use `hasNextPage` and `nextPageNumber` to decide whether more results exist:

```typescript theme={null}
const response = await fetch(`${baseUrl}?pageNumber=1&pageSize=20`, { headers });
const data = await response.json();

if (data.hasNextPage) {
  // Fetch the next page using data.nextPageNumber
}
```

### Fetching All Pages

To retrieve every item across all pages, loop until `hasNextPage` is `false`:

```typescript theme={null}
async function fetchAllItems(baseUrl: string, headers: HeadersInit, pageSize = 20) {
  const allItems = [];
  let pageNumber = 1;

  while (true) {
    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);

    if (!data.hasNextPage) break;
    pageNumber = data.nextPageNumber ?? pageNumber + 1;
  }

  return allItems;
}
```

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

```typescript theme={null}
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

```json theme={null}
{
  "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

| Field                  | Type    | Description                                        |
| ---------------------- | ------- | -------------------------------------------------- |
| `nodes`                | array   | Items for this page                                |
| `pageInfo.hasNextPage` | boolean | Whether more results exist beyond this page        |
| `pageInfo.endCursor`   | string  | Opaque cursor to pass as `after` for the next page |
| `totalCount`           | number  | Total number of matching items across all pages    |

### Fetching the Next Page

Pass the `endCursor` from the previous response as the `after` variable on the next request, and stop when `hasNextPage` is `false`:

```typescript theme={null}
let cursor: string | null = null;

while (true) {
  const response = await fetch(graphqlUrl, {
    method: "POST",
    headers,
    body: JSON.stringify({
      operationName: "GetERPItems",
      query: ITEMS_QUERY,
      variables: { first: 20, after: cursor },
    }),
  });

  const { data } = await response.json();
  const page = data.QueryItems;

  // Process page.nodes here

  if (!page.pageInfo.hasNextPage) break;
  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:

```typescript theme={null}
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.

```typescript theme={null}
// 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

| Question                       | REST (page-based)          | GraphQL (cursor-based)            |
| ------------------------------ | -------------------------- | --------------------------------- |
| How do I set page size?        | `pageSize` query parameter | `first` variable                  |
| How do I get the next page?    | Use `nextPageNumber`       | Use `endCursor` as `after`        |
| How do I know when I'm done?   | `hasNextPage` is `false`   | `pageInfo.hasNextPage` is `false` |
| What's the total count?        | `totalItems`               | `totalCount`                      |
| Can I jump to a specific page? | Yes (`pageNumber=5`)       | No (traverse from the start)      |

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Rate Limiting" icon="gauge" href="/guides/essentials/common-patterns/rate-limiting">
    Understand request quotas, especially important when fetching many pages.
  </Card>

  <Card title="Error Handling" icon="triangle-alert" href="/guides/essentials/common-patterns/error-handling">
    Handle errors that may occur during paginated fetches.
  </Card>

  <Card title="Using Platform APIs" icon="database" href="/guides/cloud-apps/using-platform-apis">
    Full guide to querying the GraphQL API with filtering, sorting, and pagination.
  </Card>

  <Card title="Webhooks" icon="bell" href="/guides/essentials/common-patterns/webhooks">
    Instead of polling pages for changes, use webhooks to get notified in real time.
  </Card>
</CardGroup>
