Skip to content

Pagination

The data / links / meta envelope, per_page, and the endpoints that do not paginate

Updated View as Markdown

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:

{
  "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": "« 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 »", "page": 2, "active": false }
    ],
    "path": "https://api.firearmcart.com/v1/orders",
    "per_page": 25,
    "to": 25,
    "total": 87
  }
}
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 (« Previous, Next »). 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
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:

{
  "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:

{
  "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.


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

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.


  • Rate limits — why per_page=100 matters
  • Conventions — what the ids and amounts inside data mean
  • Errors — what a failed page request looks like
Navigation

Type to search…

↑↓ navigate↵ selectEsc close