Skip to content

Conventions

The two identifier kinds and the mixed money units, resource by resource

Updated View as Markdown

Two conventions on this API need a lookup table, and they are the two most common causes of a broken integration.

  1. IDs. Identifiers are either per-store sequential integers or UUIDs — nothing else. Which one a resource uses is fixed per resource.
  2. 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 on order_id alone 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 clean 404. 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 a 500. (GET /v1/fulfillments/{fulfillment_id} does guard the format and returns 404.) Send the uuid and 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 own shipping_address.
  • The subscription’s payment_method object carries payment_method_id alongside brand and last_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.2 problems 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.

  • Pagination — the envelope these fields arrive in
  • Errors — what a bad id looks like coming back
  • Webhooks — where these conventions differ
Navigation

Type to search…

↑↓ navigate↵ selectEsc close