An order is the record of a completed checkout: who bought, what they bought, what was charged, and how it shipped. Two of the three endpoints here are read-only; the third, the upsell endpoint, is the one place in the API where an order is mutated and a card is charged.
Reach for orders when you are reconciling revenue, driving an order-status page, or syncing into an ERP. If all you need is tracking data, the fulfillments endpoints are cheaper and more direct.
Identifiers
Orders are addressed by order_id: a per-store
sequential number that matches what the dashboard shows. It is not a database primary
key, and it is only unique within your team. The same convention holds for
customer_id, product_id and variant_id.
Three fields use UUIDs instead: fulfillment_id, transaction_id and
reference_transaction_id. Treat them as opaque strings — they are stable, globally
unique, and deliberately not sequential. No field on an order is a database primary key.
Money
Orders emit money as floating-point dollars — "total": 644.46 means $644.46. This
covers subtotal, tax, shipping, shipping_insurance, surcharge, discount, total, every
items[].price and items[].subtotal, and every transactions[].amount.
The upsell endpoint is the exception, and it is inconsistent in both directions: its
request body takes items[].price and tax in integer cents, and the
transaction.amount it returns is also in integer cents, while the order object in
that same response is still in dollars. Parse the two halves separately.
Values are JSON numbers, so a whole-dollar amount serializes without a fractional part
("discount": 25, not 25.00). Do not rely on the decimal places to detect the unit.
Order status
The status on every order is not the stored column. It is re-derived from the order’s
transaction ledger on each read, because reversals are recorded as separate rows rather
than by mutating the original payment. The derived value is one of:
pending, awaiting_payment, failed, completed, refunded,
partially_refunded, void, partially_void, chargeback, partially_chargeback
Externally settled marketplace orders — currently GunBroker imports — carry no local
payment ledger, so they report their stored status instead. That is the only way values
like paid or shipped appear.
This matters when filtering: ?status= is matched against the stored column, so a
filtered list can contain orders whose returned status is something else. Filter to
narrow the result set, then branch on the value you actually get back.
What is included
List results embed items, transactions, the saved card summary and both addresses.
They do not include fulfillments — that key is omitted entirely rather than
returned empty. Only the single-order endpoint loads fulfillments and their shipments.
Addresses carry street, city, state and ZIP only. There is no recipient name, company, country or phone behind the address object, so a label cannot be reconstructed from an order response alone.
Errors
Orders use the platform-wide error envelope of error plus message, with one
inconsistency worth coding around: a missing order is not_found on
GET /v1/orders/{order_id} but order_not_found on the upsell endpoint. Match on the
HTTP status, not the string.
Malformed upsell bodies return the stock validation shape (message plus a keyed
errors object) rather than the error/message envelope, and rate limiting returns a
bare {"message": "Too Many Attempts."}. See Errors for the full
picture and Authentication for how abilities are granted.
List orders
/v1/orders- Required ability
orders:read- Rate limit
- 60 requests/minute per team
Query parameters
customer_idintegeroptional- Only orders belonging to this customer. This is the `customer_id` returned by the Customers endpoints, not a database id.Example:
1001 statusstringoptional- Exact match against the order's stored status column. Accepted values: pending, awaiting_payment, completed, paid, shipped, failed, void, partially_void, refunded, partially_refunded, chargeback, partially_chargeback. This filters the stored column, while the status in the response is re-derived from the transaction ledger, so the two can disagree.Example:
completed created_sincestringoptional- Only orders created at or after this timestamp. The value is passed straight to the database, so send an ISO 8601 timestamp or a plain yyyy-mm-dd date in UTC. There is no validation layer here: an unparseable value produces a 500, not a 422.Example:
2026-08-01T00:00:00Z pageintegeroptional- Page number, starting at 1. Anything that is not a positive integer falls back to page 1.Example:
2 per_pageintegeroptional- Results per page. Defaults to 25 and is capped at 100. There is no lower bound, and out-of-range values are not rejected: see the caveats below.Example:
50
- The fulfillments key is absent from list results entirely, because this endpoint does not eager-load that relation. Use the single-order endpoint, or the Fulfillments endpoints, when you need shipment data.
- Pagination links carry only the page parameter. Your customer_id, status and created_since filters are dropped from links.next, so re-send them yourself when walking pages.
- per_page is capped at 100 but has no floor and is never validated. A value of 0, or any non-numeric string, silently falls back to an internal default of 15 rather than the documented 25; a negative value removes the LIMIT altogether and returns every matching order in a single response.
- status is recomputed from the order's transaction ledger on every read, so it can differ from the stored value you filtered on. The derived value is one of pending, awaiting_payment, failed, completed, refunded, partially_refunded, void, partially_void, chargeback, partially_chargeback. Externally settled marketplace orders (source gunbroker) are the exception and report their stored status instead, which is how paid and shipped can appear.
- shipping is the sum of the order's standard shipping and its FFL transfer shipping. The two are not exposed separately.
curl -G https://api.firearmcart.com/v1/orders \
-H "Authorization: Bearer $FIREARMCART_TOKEN" \
-H "Accept: application/json" \
-d status=completed \
-d created_since=2026-08-01T00:00:00Z \
-d per_page=25{
"data": [
{
"order_id": 5001,
"customer_id": 1001,
"status": "completed",
"subtotal": 599.97,
"tax": 49.5,
"shipping": 19.99,
"shipping_insurance": 0,
"surcharge": 0,
"surcharge": 0,
"discount": 25,
"total": 644.46,
"items": [
{
"product_id": 501,
"variant_id": 1201,
"name": "Glock 19 Gen 5 9mm Pistol",
"variant_name": "Finish: Black / Capacity: 15rd",
"sku": "GLK-PA195S201-BLK",
"quantity": 1,
"price": 549.99,
"subtotal": 549.99,
"is_cancelled": false,
"is_recurring": false,
"properties": null
},
{
"product_id": 733,
"variant_id": null,
"name": "Federal American Eagle 9mm 115gr, 50rd",
"variant_name": null,
"sku": "FED-AE9DP",
"quantity": 2,
"price": 24.99,
"subtotal": 49.98,
"is_cancelled": false,
"is_recurring": false,
"properties": null
}
],
"transactions": [
{
"transaction_id": "a7c3e9f1-52b8-4d6a-9e04-8b1f6c2d3a90",
"type": "payment",
"status": "completed",
"amount": 644.46,
"gateway_transaction_id": "11ef9c2a-4d31-4f27-9a10-6c1f0b8e2d55",
"reference_transaction_id": null,
"created_at": "2026-08-04T15:22:10+00:00"
}
],
"payment_method": {
"brand": "visa",
"last_four": "4242"
},
"shipping_address": {
"address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
"type": "shipping",
"address_line1": "1200 Market Street",
"address_line2": "Suite 400",
"city": "Chattanooga",
"state": "TN",
"zip_code": "37402",
"is_billing": false,
"is_shipping": true,
"is_default_billing": false,
"is_default_shipping": true
},
"billing_address": {
"address_id": "a3f82c50-1d6e-47b9-8e42-9c0d7b3e5f18",
"type": "billing",
"address_line1": "88 Riverfront Parkway",
"address_line2": null,
"city": "Chattanooga",
"state": "TN",
"zip_code": "37402",
"is_billing": true,
"is_shipping": false,
"is_default_billing": true,
"is_default_shipping": false
},
"created_at": "2026-08-04T15:22:08+00:00",
"updated_at": "2026-08-05T09:14:33+00:00"
}
],
"links": {
"first": "https://api.firearmcart.com/v1/orders?page=1",
"last": "https://api.firearmcart.com/v1/orders?page=2",
"prev": null,
"next": "https://api.firearmcart.com/v1/orders?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 2,
"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": 40
}
}{
"message": "Invalid ability provided."
}{
"message": "Too Many Attempts."
}Retrieve an order
/v1/orders/{order_id}- Required ability
orders:read- Rate limit
- 60 requests/minute per team
Path parameters
order_idintegerrequired- The order's `order_id`: the per-store sequential number shown in the dashboard. Not the database primary key.Example:
5001
- The path segment must be numeric. No route constraint enforces it, so a value such as /v1/orders/abc fails type coercion in the controller and returns a 500 rather than a 404.
- fulfillment_id, transaction_id and reference_transaction_id are UUIDs. order_id, customer_id, product_id and variant_id are store-scoped numbers. No field on this resource is a database primary key.
- Addresses expose only street, city, state and ZIP. There is no recipient name, company, country or phone column behind the address resource, so those never appear.
- An address's type is derived from its role flags rather than stored: an address that is billing-only reports billing and anything else reports shipping. Read is_billing and is_shipping when one address serves both roles.
- A nested fulfillment's fulfillment_type is the supplier that ships it — the distributor plugin's slug (rsr, chattanooga, lipseys, 2aw, eprolo, mge, printify, shipstation) or internal for stock you ship yourself. `mge` is always a warehouse-restock leg (MGE ships to the store, not the customer). It is not a destination flag, so there is no ffl value; whether the shipment routes through a dealer is not exposed on this resource. The sibling service field is a legacy column no production code path writes, so expect null.
- variant_name is built from the variant's option values (for example "Finish: Black / Capacity: 15rd"), not from the variant's name column. A variant with no options yields an empty string rather than null.
- An order with no saved card reports payment_method as null, but a missing address does NOT report null. shipping_address and billing_address are wrapped with the single-argument form of whenLoaded, which hands the null relation straight to the address resource, and that resource has no null guard — so an order whose address row is missing or soft-deleted returns a full address object with every field null (and is_billing/is_shipping false) rather than the null you would expect. Test address_line1 for null, not the object itself.
curl https://api.firearmcart.com/v1/orders/5001 \
-H "Authorization: Bearer $FIREARMCART_TOKEN" \
-H "Accept: application/json"{
"data": {
"order_id": 5001,
"customer_id": 1001,
"status": "completed",
"subtotal": 599.97,
"tax": 49.5,
"shipping": 19.99,
"shipping_insurance": 0,
"surcharge": 0,
"discount": 25,
"total": 644.46,
"items": [
{
"product_id": 501,
"variant_id": 1201,
"name": "Glock 19 Gen 5 9mm Pistol",
"variant_name": "Finish: Black / Capacity: 15rd",
"sku": "GLK-PA195S201-BLK",
"quantity": 1,
"price": 549.99,
"subtotal": 549.99,
"is_cancelled": false,
"is_recurring": false,
"properties": null
},
{
"product_id": 733,
"variant_id": null,
"name": "Federal American Eagle 9mm 115gr, 50rd",
"variant_name": null,
"sku": "FED-AE9DP",
"quantity": 2,
"price": 24.99,
"subtotal": 49.98,
"is_cancelled": false,
"is_recurring": false,
"properties": null
}
],
"transactions": [
{
"transaction_id": "a7c3e9f1-52b8-4d6a-9e04-8b1f6c2d3a90",
"type": "payment",
"status": "completed",
"amount": 644.46,
"gateway_transaction_id": "11ef9c2a-4d31-4f27-9a10-6c1f0b8e2d55",
"reference_transaction_id": null,
"created_at": "2026-08-04T15:22:10+00:00"
}
],
"payment_method": {
"brand": "visa",
"last_four": "4242"
},
"shipping_address": {
"address_id": "6e1d9f27-3c4b-4a85-b1e0-5d8c2f7a94b3",
"type": "shipping",
"address_line1": "1200 Market Street",
"address_line2": "Suite 400",
"city": "Chattanooga",
"state": "TN",
"zip_code": "37402",
"is_billing": false,
"is_shipping": true,
"is_default_billing": false,
"is_default_shipping": true
},
"billing_address": {
"address_id": "a3f82c50-1d6e-47b9-8e42-9c0d7b3e5f18",
"type": "billing",
"address_line1": "88 Riverfront Parkway",
"address_line2": null,
"city": "Chattanooga",
"state": "TN",
"zip_code": "37402",
"is_billing": true,
"is_shipping": false,
"is_default_billing": true,
"is_default_shipping": false
},
"fulfillments": [
{
"fulfillment_id": "9c2f1a44-73d1-4c8e-9d3e-2f5b8a61c7aa",
"order_id": 5001,
"status": "shipped",
"fulfillment_type": "rsr",
"service": null,
"shipments": [
{
"tracking_number": "1Z999AA10123456784",
"carrier": "ups",
"tracking_url": "https://www.ups.com/track?tracknum=1Z999AA10123456784",
"shipped_at": "2026-08-05T09:14:31+00:00"
}
],
"timeline": [],
"notes": null,
"processed_at": "2026-08-04T18:03:00+00:00",
"shipped_at": "2026-08-05T09:14:31+00:00",
"delivered_at": null,
"delivery_failed": false,
"delivery_reason": null,
"created_at": "2026-08-04T15:22:12+00:00",
"updated_at": "2026-08-05T09:14:33+00:00"
}
],
"created_at": "2026-08-04T15:22:08+00:00",
"updated_at": "2026-08-05T09:14:33+00:00"
}
}{
"error": "not_found",
"message": "Order not found."
}Add a post-purchase upsell
/v1/orders/{order_id}/upsell- Required ability
orders:upsell- Rate limit
- 30 requests/minute per team
Path parameters
order_idintegerrequired- The `order_id` of the order to extend. It must already carry a completed payment, or be an externally settled marketplace order.Example:
5001
Body parameters
itemsarray<object>required- The add-on line items. At least one entry is required.
items[].product_idintegerrequired- The product's `product_id`. It must belong to the same store as the order, or the request fails with upsell_invalid.Example:
733 items[].variant_idintegeroptional- The variant's `variant_id`, which is scoped to the product above. Omit it or send null for a product without variants.Example:
1201 items[].quantityintegerrequired- Units to add. Minimum 1.Example:
1 items[].priceintegeroptional- Unit price override in CENTS. Minimum 0. Omit it to charge the variant's price, or the product's price when no variant is given.Example:
2499 taxintegeroptional- Tax for the add-on, in CENTS. Minimum 0. Nothing is calculated for you: omit it and the add-on is charged with zero tax. Add-ons never carry shipping.Example:
206 idempotency_keystringrequired- Your own unique key for this add-on, up to 255 characters. Replaying it returns the original result instead of charging again. Read the caveats before choosing a key format.Example:
addon_5001_1a2b3c4d
- Units are mixed inside one exchange. You send items[].price and tax in integer cents, the order object comes back in float dollars, and the top-level transaction.amount is in integer cents. Do not reuse one parser for both.
- The idempotency key is looked up across the whole team, not per order: any completed transaction carrying that key wins. Reuse a key from a different order, or from a checkout charge, and you get a 200 with replayed true, no new line items, and a transaction belonging to some other order. Namespace your keys per order.
- A decline records a failed transaction that stores no idempotency key, so declines are deliberately not covered by the replay guard: retrying a declined add-on with the same key genuinely re-attempts the charge.
- orders:upsell is not one of the default token abilities. Grant it explicitly when creating the token.
- The order object in this response omits shipping_address, billing_address and fulfillments, because the reloaded model only eager-loads items, transactions and the payment method. Call the retrieve endpoint when you need them.
- The card charged is the order's saved payment method, falling back to the customer's most recently created active card. The gateway is the one the original payment settled on, falling back to the team's active card merchant account. If neither can be resolved, the request fails with upsell_invalid.
- A blacklisted customer or IP is rejected before the gateway is contacted, but that guard throws a non-JSON error the controller cannot classify, so it surfaces as a 500 with error processing_error rather than a 422.
- Add-on items are tagged with a properties object of { "_upsell": true }, which is how you tell them apart from the original lines.
curl -X POST https://api.firearmcart.com/v1/orders/5001/upsell \
-H "Authorization: Bearer $FIREARMCART_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"items": [
{ "product_id": 733, "quantity": 1, "price": 2499 }
],
"tax": 206,
"idempotency_key": "addon_5001_1a2b3c4d"
}'{
"success": true,
"replayed": false,
"order": {
"order_id": 5001,
"customer_id": 1001,
"status": "completed",
"subtotal": 574.98,
"tax": 47.43,
"shipping": 19.99,
"shipping_insurance": 0,
"surcharge": 0,
"discount": 0,
"total": 642.4,
"items": [
{
"product_id": 501,
"variant_id": 1201,
"name": "Glock 19 Gen 5 9mm Pistol",
"variant_name": "Finish: Black / Capacity: 15rd",
"sku": "GLK-PA195S201-BLK",
"quantity": 1,
"price": 549.99,
"subtotal": 549.99,
"is_cancelled": false,
"is_recurring": false,
"properties": null
},
{
"product_id": 733,
"variant_id": null,
"name": "Federal American Eagle 9mm 115gr, 50rd",
"variant_name": null,
"sku": "FED-AE9DP",
"quantity": 1,
"price": 24.99,
"subtotal": 24.99,
"is_cancelled": false,
"is_recurring": false,
"properties": {
"_upsell": true
}
}
],
"transactions": [
{
"transaction_id": "a7c3e9f1-52b8-4d6a-9e04-8b1f6c2d3a90",
"type": "payment",
"status": "completed",
"amount": 615.35,
"gateway_transaction_id": "11ef9c2a-4d31-4f27-9a10-6c1f0b8e2d55",
"reference_transaction_id": null,
"created_at": "2026-08-04T15:22:10+00:00"
},
{
"transaction_id": "c1d8f4a2-7e39-4b06-a5c7-9f2e0d6b8a41",
"type": "payment",
"status": "completed",
"amount": 27.05,
"gateway_transaction_id": "11ef9c2a-77b0-4a19-8c31-2f7e5d9a1b04",
"reference_transaction_id": null,
"created_at": "2026-08-04T15:41:02+00:00"
}
],
"payment_method": {
"brand": "visa",
"last_four": "4242"
},
"created_at": "2026-08-04T15:22:08+00:00",
"updated_at": "2026-08-04T15:41:02+00:00"
},
"transaction": {
"transaction_id": "c1d8f4a2-7e39-4b06-a5c7-9f2e0d6b8a41",
"gateway_transaction_id": "11ef9c2a-77b0-4a19-8c31-2f7e5d9a1b04",
"amount": 2705,
"status": "completed"
}
}{
"success": false,
"error": "payment_declined",
"message": "Insufficient funds",
"details": {
"response_code": "051",
"response_message": "Insufficient funds"
}
}{
"success": false,
"error": "in_progress",
"message": "Another add-on for this order is currently processing. Please retry."
}{
"success": false,
"error": "order_not_found",
"message": "Order not found."
}
