---
title: "Conventions"
description: "The two identifier kinds and the mixed money units, resource by resource"
---

> API Reference Index
> Fetch the complete API index at: https://docs.firearmcart.com/api/llms.txt
> Use this file to discover all available pages before exploring further.

# Conventions

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:

```json
{
  "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:

```bash
# 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:

```json
{
  "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](/api/webhooks#amounts-are-strings).

---

## Dates and times

Every timestamp is ISO 8601 in **UTC**, with an explicit `+00:00` offset rather than a `Z` suffix:

```json
{ "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](/api/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](/api/pagination) — the envelope these fields arrive in
- [Errors](/api/errors) — what a bad id looks like coming back
- [Webhooks](/api/webhooks) — where these conventions differ

Source: https://docs.firearmcart.com/api/conventions/index.mdx
