API docs

Conventions

Requests

  • JSON bodies with Content-Type: application/json.
  • GET for reads, POST to create or take an action, PATCH to change part of a record, DELETE to 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": 57900 means $579.00. Fields ending in _cents never hold decimals.
  • Tax rates are in basis points: "tax_rate_bps": 725 means 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:

  1. Note the current time as T before you start.
  2. Page through GET /v1/products?updated_since=<last T> to the end.
  3. Store T as 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 /v1 contract. Within /v1 we 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 (currently 2026-09-24).