Two conventions on this API need a lookup table, and they are the two most common causes of a broken integration.
- IDs. Identifiers are either per-store sequential integers or UUIDs — nothing else. Which one a resource uses is fixed per resource.
- Money. Some amounts are integer cents. Some are float dollars. A single payment response contains both.
This page is the lookup table for both.
Identifiers
The two kinds
| Kind | Looks like | Unique across | Guessable |
|---|---|---|---|
| Store-scoped id | 1042 |
One store only | Yes — sequential |
| UUID | 9b1f4c2e-… |
Every store | No |
The rule: resources you can see numbered in the dashboard (orders, customers, products, variants, collections, zones) use their store-scoped number. Everything else uses a UUID. The API never exposes a database primary key, so no identifier reveals row counts or invites enumeration.
Warning: A store-scoped id is not globally unique. Two different stores each have an order
1001. If you store FirearmCart ids in your own database, key on the pair (store, id) or on your own primary key. Keying onorder_idalone will collide the moment you onboard a second store.
Which endpoints use which
| Field | Kind | Notes |
|---|---|---|
customer_id |
Store-scoped | Path param on /v1/customers/{customer_id}; filter on orders and subscriptions |
order_id |
Store-scoped | Path param on /v1/orders/{order_id} and /v1/orders/{order_id}/upsell |
product_id |
Store-scoped | Path param on /v1/products/{product_id}; request field on charge and upsell |
variant_id |
Store-scoped | Request field on charge and upsell |
collection_id |
Store-scoped | collection block on a product; collection_id filter |
zone_id |
Store-scoped | Shipping zones and tax zones |
subscription_id |
Store-scoped | Display number on a subscription. Not accepted by any route — use the uuid. |
fulfillment_id |
UUID | Path param on /v1/fulfillments/{fulfillment_id} |
rate_id |
UUID | From GET /v1/shipping. Send it back as shipping_rate_id on a charge. |
transaction_id |
UUID | On order.transactions[] and on the transaction object of a charge or upsell. reference_transaction_id points at another transaction’s UUID. |
address_id |
UUID | On every address object; send it back as shipping_address_id on a subscription PATCH |
payment_method_id |
UUID | Inside a subscription’s payment_method; send it back on a subscription PATCH |
uuid on a subscription |
UUID | The path param for all seven subscription endpoints |
| FFL dealer | — | No surrogate id. license_number is the identifier, and no v1 endpoint accepts an FFL id as input. |
Subscriptions carry two ids — use the UUID
GET /v1/subscriptions returns both:
{
"uuid": "9b1f4c2e-6a71-4f0a-8e2d-5c1b7a3f9d40",
"subscription_id": 27,
"status": "active"
}Every subscription route takes the uuid. The subscription_id is informational — it is the store-scoped number the dashboard displays, and it is not accepted anywhere in the API.
Warning: Passing a
subscription_id, or any other value that is not a well-formed UUID, is not a clean404. The subscription routes hand the path segment straight to a UUID-typed column with no format check, so the database rejects it and the request fails as a500. (GET /v1/fulfillments/{fulfillment_id}does guard the format and returns404.) Send theuuidand this never arises.
shipping_rate_id on a charge
POST /v1/payments/charge accepts shipping_rate_id. That value is the rate_id from GET /v1/shipping — the rate’s UUID:
# 1. Read the rates for the zone you're shipping to.
curl "https://api.firearmcart.com/v1/shipping?state=TX" \
-H "Authorization: Bearer $FIREARMCART_TOKEN" \
-H "Accept: application/json"
# → data[0].rates[0].rate_id = "4f8a2d17-9b3c-4e51-a6d8-0c7e5b2f9a14", .price = 1295 (cents)
# 2. Send that rate_id back on the charge.
# "shipping_rate_id": "4f8a2d17-9b3c-4e51-a6d8-0c7e5b2f9a14"The rate is re-resolved and verified against your store on the server, and its price is applied to the order. If you also send a shipping override, the override wins on the amount but the shipping_rate_id still has to be one of your own rates.
Updating a subscription’s address or card
PATCH /v1/subscriptions/{uuid} accepts shipping_address_id and payment_method_id. Both are UUIDs, and both are discoverable in v1 responses:
- Every address object carries
address_id— on the customer, on an order, and on the subscription’s ownshipping_address. - The subscription’s
payment_methodobject carriespayment_method_idalongsidebrandandlast_four.
Both must belong to the subscription’s customer; a UUID that exists but belongs to someone else is a 422, not a cross-customer update.
Money units
There is no currency field anywhere in the API. Everything is USD.
Integer cents
| Field | Resource |
|---|---|
price, compare_at_price |
Product |
price, compare_at_price |
Variant |
price, min_value, max_value |
Shipping rate |
amount |
The top-level transaction object on a charge or upsell response |
Float dollars
| Field | Resource |
|---|---|
subtotal, tax, shipping, shipping_insurance, surcharge, discount, total |
Order |
price, subtotal |
Order line item |
amount |
Transaction, inside order.transactions[] |
items[].unit_price |
Subscription |
The trap
A successful POST /v1/payments/charge returns both formats in one body:
{
"success": true,
"order": {
"order_id": 1042,
"subtotal": 299.99,
"shipping": 12.95,
"tax": 17.05,
"total": 329.99
},
"transaction": {
"transaction_id": "a7c3e9f1-52b8-4d6a-9e04-8b1f6c2d3a90",
"gateway_transaction_id": "ch_1PXyZ",
"amount": 32999,
"status": "completed"
}
}order.total is 329.99 dollars. transaction.amount is 32999 cents. They are the same money. POST /v1/orders/{order_id}/upsell has the identical split.
Note also that order.transactions[] — when a response includes it — reports amount in dollars, so the same transaction reads as 329.99 there and 32999 in the top-level transaction object.
Tip: Normalise at the boundary. Convert everything to integer cents the moment a response is parsed, and never do arithmetic on the float dollars —
0.1 + 0.2problems are real in an order total.
Requests are always cents
Every monetary input is an integer number of cents, on every endpoint:
| Field | Endpoint |
|---|---|
items.*.price |
charge, upsell |
shipping |
charge |
tax |
charge, upsell |
So a charge you send in cents comes back described in dollars. "price": 29999 on the way in is "price": 299.99 on the way out.
Values that are not money
| Field | Resource | Unit |
|---|---|---|
rate |
Tax zone | Decimal fraction — 0.0825 is 8.25% |
rate_percent |
Tax zone | Percent — 8.25 is 8.25% |
discount_pct |
Subscription | Percent — 10.00 is 10% off |
distance |
FFL dealer | Statute miles, present only on a zip search |
Shipping is pre-summed
An order’s shipping is the store’s shipping charge plus the FFL transfer shipping charge, added together. They are stored separately and no field separates them in the API response. If you reconcile against a shipping rate you selected, expect the order’s shipping to be larger when the order routed through an FFL.
Outgoing webhooks use a third format
Webhook payloads deliver order amounts as strings of dollars ("329.99") — not floats and not cents. See Webhooks.
Dates and times
Every timestamp is ISO 8601 in UTC, with an explicit +00:00 offset rather than a Z suffix:
{ "created_at": "2026-08-11T15:30:00+00:00" }Timestamps are null when unset — delivered_at on a fulfillment that has not been delivered, for example. Never assume a date field is populated.
Date filters (created_since, updated_since) are compared directly against stored UTC timestamps. Send full ISO 8601 in UTC and you will not be surprised.
Other shapes
| Convention | Detail |
|---|---|
| Envelope | Lists return data + links + meta; single resources return data. See Pagination. |
| Enums | Lowercase snake_case strings — completed, out_for_delivery, past_due. Treat unknown values as forward-compatible additions rather than errors. |
| Booleans | Real JSON true/false, not 1/0. |
| Nulls | A field that does not apply is null, not omitted and not an empty string. |
| Relationships | Nested blocks (items, transactions, variants, fulfillments, addresses) appear only when the endpoint loads them. A list endpoint and a detail endpoint for the same resource can return different sets of nested blocks. |
| Empty collections | [], not null — tags, states, specifications, timeline. |
Order status |
Derived from the order’s transaction ledger at read time, not simply read from a stored column. It reflects what actually settled, including partial reversals (partially_refunded, partially_void, partially_chargeback). |
Address type |
Derived from the is_billing / is_shipping flags rather than stored. An address that serves both roles reports "shipping"; read the two booleans if you need the distinction. |
Related documentation
- Pagination — the envelope these fields arrive in
- Errors — what a bad id looks like coming back
- Webhooks — where these conventions differ

