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
}
}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.linksis not the same thing as the top-levellinks. It is an array of page buttons meant for rendering a pager widget, and itslabelvalues contain HTML entities (« Previous,Next »). Do not parse it for navigation — use the top-levellinksobject.
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-dealersvalidatesper_pageas an integer between 1 and 100 and returns422for 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 acceptcreated_since; products additionally acceptsupdated_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')
doneAt 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 — why
per_page=100matters - Conventions — what the ids and amounts inside
datamean - Errors — what a failed page request looks like

