---
title: "Pagination"
description: "The data / links / meta envelope, per_page, and the endpoints that do not paginate"
---

> API Reference Index
> Fetch the complete API index at: https://docs.firearmcart.com/api/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

Six list endpoints are paginated. Two return a complete, unpaginated array. Detail endpoints return a single object. Knowing which is which up front saves you from writing a page loop that never terminates.

| Endpoint | Paginated |
|----------|-----------|
| `GET /v1/customers` | Yes |
| `GET /v1/orders` | Yes |
| `GET /v1/subscriptions` | Yes |
| `GET /v1/fulfillments` | Yes |
| `GET /v1/products` | Yes |
| `GET /v1/ffl-dealers` | Yes |
| `GET /v1/shipping` | **No** — every zone, every time |
| `GET /v1/taxation` | **No** — every zone, every time |

---

## The envelope

A paginated response has exactly three top-level keys:

```json
{
  "data": [
{ "order_id": 1042, "status": "completed", "total": 329.99 }
  ],
  "links": {
"first": "https://api.firearmcart.com/v1/orders?page=1",
"last": "https://api.firearmcart.com/v1/orders?page=4",
"prev": null,
"next": "https://api.firearmcart.com/v1/orders?page=2"
  },
  "meta": {
"current_page": 1,
"from": 1,
"last_page": 4,
"links": [
  { "url": null, "label": "&laquo; Previous", "page": null, "active": false },
  { "url": "https://api.firearmcart.com/v1/orders?page=1", "label": "1", "page": 1, "active": true },
  { "url": "https://api.firearmcart.com/v1/orders?page=2", "label": "2", "page": 2, "active": false },
  { "url": "https://api.firearmcart.com/v1/orders?page=2", "label": "Next &raquo;", "page": 2, "active": false }
],
"path": "https://api.firearmcart.com/v1/orders",
"per_page": 25,
"to": 25,
"total": 87
  }
}
```

### `links`

| Key | Value |
|-----|-------|
| `first` | URL of page 1 |
| `last` | URL of the final page |
| `prev` | URL of the previous page, or `null` on page 1 |
| `next` | URL of the next page, or `null` on the last page |

Follow `links.next` until it is `null`. That is the whole pagination contract.

### `meta`

| Key | Type | Value |
|-----|------|-------|
| `current_page` | integer | 1-based page number |
| `from` | integer or null | Index of the first row on this page; `null` when the page is empty |
| `last_page` | integer | Total number of pages |
| `links` | array | UI page-link descriptors — see the warning below |
| `path` | string | Base URL without query string |
| `per_page` | integer | Rows requested per page |
| `to` | integer or null | Index of the last row on this page; `null` when the page is empty |
| `total` | integer | Total matching rows across all pages |

> **Warning:** `meta.links` is **not** the same thing as the top-level `links`. It is an array of page buttons meant for rendering a pager widget, and its `label` values contain HTML entities (`&laquo; Previous`, `Next &raquo;`). Do not parse it for navigation — use the top-level `links` object.

### Empty results

An empty result set is a normal `200` with `data: []`, `from: null`, `to: null`, `total: 0` and `last_page: 1`. It is never a `404`.

---

## Request parameters

| Parameter | Type | Default | Notes |
|-----------|------|---------|-------|
| `page` | integer | `1` | Page number |
| `per_page` | integer | `25` | Clamped to a maximum of 100 |

```bash
curl "https://api.firearmcart.com/v1/products?page=2&per_page=100" \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
```

Values above 100 are silently clamped to 100 rather than rejected — you get 100 rows and `meta.per_page: 100`, not a `422`.

> **Note:** Only `GET /v1/ffl-dealers` validates `per_page` as an integer between 1 and 100 and returns `422` for anything else. The other five list endpoints only enforce the upper bound. Always send a positive integer between 1 and 100.

Requesting a page beyond `last_page` returns `200` with an empty `data` array, not a `404`.

---

## Detail endpoints are not paginated

Single-resource endpoints wrap the object in `data` and emit no `links` or `meta`:

```json
{
  "data": {
"order_id": 1042,
"status": "completed",
"total": 329.99
  }
}
```

This applies to `GET /v1/customers/{customer_id}`, `GET /v1/orders/{order_id}`, `GET /v1/fulfillments/{fulfillment_id}`, `GET /v1/products/{product_id}` and `GET /v1/subscriptions/{uuid}`.

---

## The two endpoints that return everything

`GET /v1/shipping` and `GET /v1/taxation` return every matching zone in one response, with **no** `links` and **no** `meta`:

```json
{
  "data": [
{
  "zone_id": 3,
  "name": "Continental US",
  "states": ["AL", "AK", "AZ"],
  "active": true,
  "rates": []
}
  ]
}
```

They accept no `page` or `per_page` parameter — sending one has no effect. Both payloads are small and change rarely, so cache them rather than fetching per checkout. See [Rate limits](/api/rate-limits#handling-429-in-your-client).

---

## Ordering and drift

`GET /v1/customers`, `/orders`, `/subscriptions` and `/fulfillments` are sorted **newest first** by creation time, and that order is not configurable. `GET /v1/products` defaults to newest first but accepts `sort_by` and `sort_direction`.

Because the default sort is newest-first, rows created while you are paging shift the window and can push a record onto a page you have already read, causing you to miss it. Two ways to avoid that:

- **For products**, walk oldest-first with `sort_by=created_at&sort_direction=asc`. New rows then land after your cursor rather than before it.
- **For everything else**, do incremental syncs with `created_since` (an ISO 8601 timestamp) and reconcile by id rather than relying on a single clean pass. The list endpoints for customers, orders and fulfillments all accept `created_since`; products additionally accepts `updated_since`.

There is no cursor pagination and no `ETag`/`If-None-Match` support.

---

## Paging through everything

```bash
url="https://api.firearmcart.com/v1/orders?per_page=100"

while [ -n "$url" ] && [ "$url" != "null" ]; do
  body=$(curl -s "$url" \
-H "Authorization: Bearer $FIREARMCART_TOKEN" \
-H "Accept: application/json")

  echo "$body" | jq -c '.data[]'
  url=$(echo "$body" | jq -r '.links.next')
done
```

At `per_page=100` a 5,000-order store costs 50 requests, comfortably inside one minute's budget. At the default 25 it costs 200 requests and will hit the rate limit.

---

## Related documentation

- [Rate limits](/api/rate-limits) — why `per_page=100` matters
- [Conventions](/api/conventions) — what the ids and amounts inside `data` mean
- [Errors](/api/errors) — what a failed page request looks like

Source: https://docs.firearmcart.com/api/pagination/index.mdx
