Skip to content

Orders

Read a store's orders and charge post-purchase add-ons against an order that has already been paid.

Updated View as Markdown

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

GET/v1/orders
Returns a paginated list of the authenticated team's orders, newest first. Every order arrives with its line items, transaction ledger, saved card summary and both addresses already embedded, so a single page of results is usually enough to reconcile without follow-up calls.
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
Caveats
  • 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.
Requestbash
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
200A page of orders, newest first.
{
  "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
  }
}
403The token is missing the orders:read ability. A team without API access, or with a lapsed subscription, also 403s here, with an error of forbidden or subscription_expired.
{
  "message": "Invalid ability provided."
}
429More than 60 requests in the current minute for this team. Read Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset from the response headers.
{
  "message": "Too Many Attempts."
}

Retrieve an order

GET/v1/orders/{order_id}
Returns a single order by its `order_id`, scoped to the authenticated team. This is the only orders endpoint that includes the fulfillments array and its nested shipments, so it is what you poll for tracking numbers.
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
Caveats
  • 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.
Requestbash
curl https://api.firearmcart.com/v1/orders/5001 \
  -H "Authorization: Bearer $FIREARMCART_TOKEN" \
  -H "Accept: application/json"
200The order, with fulfillments and their shipments included.
{
  "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"
  }
}
404No order with that `order_id` belongs to the authenticated team. Another team's order is indistinguishable from one that does not exist.
{
  "error": "not_found",
  "message": "Order not found."
}

Add a post-purchase upsell

POST/v1/orders/{order_id}/upsell
Appends one or more add-on line items to an order that has already been paid, and charges the card on file for just the add-on amount. The charge is recorded as a second transaction on the same order, and the order's subtotal, tax and total are incremented rather than recomputed, so an existing discount or insurance line survives untouched.
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
Caveats
  • 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.
Requestbash
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"
  }'
200The add-on was charged and the line items were added. replayed is true when a previously completed charge with the same idempotency_key was returned instead of a new one.
{
  "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"
  }
}
422Four different failures share this status, told apart by the error field: payment_declined (shown here, with a details object carrying the gateway codes), order_not_paid when the order has no completed payment, upsell_invalid when a product or variant is not found or the order has no card or merchant account to charge, and a stock validation body of message plus errors when the request itself is malformed.
{
  "success": false,
  "error": "payment_declined",
  "message": "Insufficient funds",
  "details": {
    "response_code": "051",
    "response_message": "Insufficient funds"
  }
}
409Another add-on for this order and idempotency_key is mid-flight and the 10-second lock wait expired. Retry.
{
  "success": false,
  "error": "in_progress",
  "message": "Another add-on for this order is currently processing. Please retry."
}
404No order with that `order_id` belongs to the authenticated team. Note that the error code differs from the one the retrieve endpoint returns.
{
  "success": false,
  "error": "order_not_found",
  "message": "Order not found."
}
Navigation

Type to search…

↑↓ navigate↵ selectEsc close