API docs
Conventions
Requests
- JSON bodies with
Content-Type: application/json. GETfor reads,POSTto create or take an action,PATCHto change part of a record,DELETEto remove.- Paths are resource-style:
/v1/products/123,/v1/orders/45/void.
Response envelope
Every response is JSON in the same envelope.
Success:
{ "status": "ok", "data": { "object": "product", "id": 2053, "...": "..." } }
A list adds meta:
{ "status": "ok", "data": [ { "...": "..." } ], "meta": { "limit": 50, "has_more": true, "next_cursor": "aWQ6MjEwNA" } }
Error:
{ "status": "error", "error": { "code": "INSUFFICIENT_SCOPE", "message": "This API key needs products:write access. …", "details": { "required": "products:write" } } }
Branch on error.code, which is stable. message is readable English and may change. For VALIDATION_ERROR, details maps each field to a list of messages, e.g. { "qty_delta": ["The qty_delta field is required."] }.
Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 401 | INVALID_API_KEY |
Missing, malformed, unknown, revoked or expired key |
| 401 | WRONG_ENVIRONMENT |
A live key on the sandbox, or a sandbox key on live |
| 403 | IP_NOT_ALLOWED |
The caller's IP isn't in the key's allowlist |
| 403 | INSUFFICIENT_SCOPE |
The key lacks the permission in details.required |
| 402 | API_NOT_IN_PLAN |
The shop's plan (Getting Started) doesn't include the API |
| 402 | TRIAL_EXPIRED, BILLING_PAST_DUE, SUBSCRIPTION_CANCELED, PLAN_REQUIRED |
The shop's own Iron Register subscription needs attention |
| 402 | RANGE_ADDON_REQUIRED |
/v1/range/* on a shop without the Range add-on |
| 404 | NOT_FOUND |
No such record, or it's at a location the key can't see |
| 405 | METHOD_NOT_ALLOWED |
The path exists but not with this method |
| 409 | SERIALIZED |
Tried to set the quantity of a serialized product, or to receive a serialized purchase-order line |
| 409 | PO_SUBMITTED |
Tried to change the vendor or lines of a submitted purchase order |
| 409 | DUPLICATE_PURCHASE_ORDER |
A purchase order with that flxpoint_order_id already exists |
| 409 | NOTHING_OUTSTANDING |
Nothing non-serialized is left to receive on the purchase order (details.skipped) |
| 409 | NOT_TRACKED |
Tried to change stock on a product that doesn't track inventory |
| 409 | EXTERNAL_REFERENCE_TAKEN |
external_reference collides with a non-API order; send a different one |
| 409 | FIREARM_ORDER |
Tried to void an order with firearm lines through the API |
| 409 | HAS_STOCK |
Tried to delete a product, or flip is_serialized, while stock is on hand |
| 409 | COMPLIANCE_FIELD |
Tried to change is_firearm, is_serialized, is_nfa, or product_type to or from firearm, on an existing product (details.field). These are set at creation and changed only in Iron Register. |
| 409 | SKU_TAKEN |
That SKU belongs to another product |
| 409 | DUPLICATE_CUSTOMER |
A customer with that email exists (details.existing_id) |
| 409 | INSUFFICIENT_STOCK |
Not enough stock at the location (the shop doesn't allow selling into negative stock) |
| 409 | CODE_TAKEN |
That coupon code is already in use |
| 409 | GIFT_CERTIFICATE_CODE_TAKEN |
That gift certificate code is already in use |
| 409 | FIELD_NOT_WRITABLE |
PATCH /v1/locations/{id} with a field other than phone, email or business_hours (details.fields) |
| 409 | RESOURCE_UNAVAILABLE |
The lane was taken for that time, or is out of service |
| 409 | SESSION_FULL |
Not enough seats left in the class session |
| 409 | INVALID_STATE |
The record's status doesn't allow the action (e.g. cancelling a completed reservation) |
| 409 | ALREADY_VOIDED, SALE_REFUNDED |
The order can't be voided |
| 409 | IDEMPOTENCY_IN_PROGRESS |
A request with this Idempotency-Key is still running |
| 400 | VALIDATION_ERROR |
A field is missing or invalid (see details) |
| 400 | IDEMPOTENCY_KEY_REQUIRED |
Moving a gift certificate or store credit balance (redeem, reverse, reload, adjust, issue) needs an Idempotency-Key header |
| 422 | GIFT_CERTIFICATE_INACTIVE |
The certificate is disabled, expired or depleted (details.status) |
| 422 | GIFT_CERTIFICATE_INSUFFICIENT |
Not enough balance for the redemption or negative adjustment (details.balance_cents) |
| 422 | INSUFFICIENT_CREDIT |
The customer doesn't have that much store credit |
| 422 | GIFT_CERTIFICATE_CODE_INVALID |
A requested code isn't 6–40 letters, digits or dashes |
| 422 | INVALID_REFERENCE |
A referenced record (e.g. purchaser_customer_id) doesn't exist |
| 422 | INVALID_RESOURCE |
A range resource that isn't active, or a class no longer offered |
| 422 | INVALID_SESSION |
The class session isn't open for booking |
| 422 | CONSTRAINT_VIOLATION |
The booking breaks one of the range's rules |
| 422 | EMPTY_PO |
Tried to submit a purchase order with no lines |
| 422 | IDEMPOTENCY_MISMATCH |
This Idempotency-Key was used with a different request body |
| 429 | RATE_LIMITED |
Over the per-minute or per-day limit, or more than 30 failed key attempts in a minute from one IP. Honor Retry-After. |
| 500 | SERVER_ERROR |
Something broke on our side. Safe to retry with the same Idempotency-Key. |
Money, dates, booleans
- Money is always an integer number of cents in the shop's currency (USD):
"price_cents": 57900means $579.00. Fields ending in_centsnever hold decimals. - Tax rates are in basis points:
"tax_rate_bps": 725means 7.25%. - Timestamps are ISO-8601 in UTC with a
Z:"2026-09-24T18:03:11Z". - Calendar dates (dates of birth, expiry dates, PO dates) are
YYYY-MM-DD. - Booleans are JSON
true/false. - Resource type. Every object carries
object("product","order", …) naming its type.
Pagination
Lists are cursor-paginated in ascending id order. Records created while you page don't shift or duplicate what you've already read.
| Parameter | Meaning |
|---|---|
limit |
1–200, default 50 |
cursor |
The previous page's meta.next_cursor |
To read everything, follow next_cursor while has_more is true:
curl "https://app.ironregister.com/api/v1/products?limit=200" -H "Authorization: Bearer $KEY"
curl "https://app.ironregister.com/api/v1/products?limit=200&cursor=aWQ6MjI1Mw" -H "Authorization: Bearer $KEY"
Cursors are opaque; don't build them yourself. A bad cursor returns 400 VALIDATION_ERROR.
Incremental sync with updated_since
Most lists take updated_since (ISO-8601). It returns records changed at or after that moment. On lists without an updated_at field (returns, stock movements, events), it filters on creation time.
A reliable sync loop:
- Note the current time as
Tbefore you start. - Page through
GET /v1/products?updated_since=<last T>to the end. - Store
Tas the new "last T".
Records changed during the run are picked up again next time. That's harmless if your writes are upserts keyed by id. For near-real-time changes, pair this with webhooks, and keep the periodic sync as a safety net.
Idempotency
Any POST can take an Idempotency-Key header of up to 100 characters. A UUID works well.
- Same key, same body, within 24 hours: you get the stored response back, with the header
Idempotent-Replayed: true. The write doesn't happen twice. - Same key, different body:
422 IDEMPOTENCY_MISMATCH. - Same key while the first request is still running:
409 IDEMPOTENCY_IN_PROGRESS. - Failures aren't stored. A 5xx or a thrown error isn't saved, so retrying with the same key is safe.
Keys are scoped to your API key. POST /v1/orders has its own natural idempotency as well: external_reference (see Orders).
Rate limits
Limits are per shop and shared by all of its keys:
- Established: 60 requests per minute and 10,000 per day.
- Enterprise: 600 per minute and no daily cap.
Every authenticated response carries:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Requests allowed per minute |
X-RateLimit-Remaining |
Requests left in this minute |
X-RateLimit-Reset |
Unix time the minute window resets |
X-RateLimit-Daily-Limit |
Established only: requests allowed per UTC day |
X-RateLimit-Daily-Remaining |
Established only: requests left today (resets 00:00 UTC) |
Over the limit you get 429 RATE_LIMITED with Retry-After in seconds. Back off for that long.
Separately, more than 30 failed key attempts from one IP in a minute also returns 429.
Versioning
- The
/v1contract. Within/v1we only make additive changes: new endpoints, new fields, new event types and new enum values. Ignore unknown fields. - Breaking changes get a new path version, announced in the changelog.
- Webhook version. Webhook payloads carry
api_version(currently2026-09-24).