Skip to content

REST API

Enterprise plan

Floormode exposes its operations over HTTP at /api/v1. It is not a separate, thinner copy of the app — the API and the app run the same operations, so anything the interface can do, an integration can do, and neither can drift ahead of the other.

  • What the API is
  • Authenticating
  • Requests
  • Responses and errors
  • Limits
  • Discovering the surface
  • Changes
  • See also

Orders, allocations, picking, packing, shipments, receiving, returns, inventory movements, counts, replenishment, analytics — the same operations and the same vocabulary the app’s screens use. Each operation says whether it reads or writes.

Two properties are worth knowing before you build anything against it:

  • Operation names are the API. orders_list is the operation id in REST, the method app.orders.list an agent calls over the MCP server (the first _ becomes a dot), and the handler name inside the app. One operation, three doors.
  • The tenant comes from your credentials, never from the URL. There is no shop or account parameter anywhere in the surface. A token identifies exactly one Floormode account, and a request can only ever reach that account’s data.

Every request carries a bearer token:

GET /api/v1/orders?status=unfulfilled HTTP/1.1
Host: your-app-domain
Authorization: Bearer <token>

Two token types are accepted. External integrations and agents use a WorkOS OAuth access token — discovery metadata is published at /.well-known/oauth-protected-resource (RFC 9728), so a standards-aware client can find the authorization server on its own. The embedded app uses its Shopify session token. A request with neither is answered 401.

GET and DELETE operations take their input from the query string; POST, PUT and PATCH take a JSON body.

Query values are coerced against each parameter’s declared type before validation, so ?limit=25 reaches a numeric field as the number 25, and ?includeArchived=true reaches a boolean as true. Arrays accept a repeated parameter (?ids=a&ids=b) or a JSON array.

One behaviour deserves its own paragraph, because it is deliberately stricter than most APIs: a query parameter the operation does not declare is refused, not ignored.

{
"error": "Unknown query parameter(s): stat. orders_list accepts: status, cursor.",
"code": "unknown_parameter",
"unknown": ["stat"],
"accepted": ["status", "cursor"]
}

Silently dropping stat=open would return the whole unfiltered list with a 200 — a wrong answer wearing a correct answer’s clothes. The refusal names what the operation does accept, so one round trip fixes a typo.

A success is always wrapped:

{ "data": { "orders": [], "cursor": null } }

A failure is always {"error": "<human message>"}, plus a code whenever the refusal is one the app classified. Branch on code, never on the message text — messages are written for people and will be reworded.

codeHTTPMeans
invalid_input400A domain precondition the operation checked
unknown_parameter400A query parameter the operation does not declare
unauthorized401No usable identity
plan_required403The account’s plan does not include this
forbidden403Authenticated, but the wrong kind of caller for this operation
not_found404The addressed record does not exist
conflict409A state or uniqueness conflict — the write cannot be applied as asked
payload_too_large413The write would exceed a storage ceiling
rate_limited429The store’s calls for this minute are used up — wait and send it again
internal500Our bug

An input that does not fit the operation’s schema is the one error without a code: it is answered 400 with {"error": "Invalid input", "issues": [...]}, the schema’s own list of what failed.

A 409 usually carries guidance in its message: it is how the API says “this order is already packed” or “use the component-pick operation for a kit line” rather than failing opaquely.

Each store can make 60 calls a minute through the API. Every REST operation call that passes validation counts, and so does every app.* call inside an MCP run; the catalog (GET /api/v1), describe_api and the app’s own screens do not. A call past the limit is answered 429 with a Retry-After: 60 header and "code": "rate_limited" (over MCP: RATE_LIMITED). Nothing was run, so wait the minute and send the same call again.

GET /api/v1 returns the machine-readable catalog — every operation with its method, path, minimum plan and input JSON Schema, straight from the running app. It needs a token but no plan, so a client can see the whole surface before its plan allows every call.

The same information, as a static document you can point tooling at:

The document is regenerated from the registry on every change, so an operation that exists in the app exists in the document, with the same description the app carries. It cannot go stale without the app’s build failing.

The API follows the app: an operation that changes shape changes it for every door at once, and there is no compatibility layer. Recent changes, October 2026:

  • MCP: describe_api without input answers an index of every method, reads apart from writes. describe_api({ namespaces: ["orders"] }) declares those namespaces with their input and return types, and describe_api({ search: "short stock" }) the methods whose name or description holds every word. The server’s instructions carry the index instead of the whole declaration.
  • picking_create_batch no longer takes filter, and its input is strict: a call that still sends filter: "singles" is answered 400 (Invalid input, with the schema’s issues). Cut single-item orders with a pick profile instead (the Singles template: units = 1) and pass its profileId. Without foDocIds the batch takes the oldest ready orders first, and before it does it re-plans up to ten reservations that went stale (a lot that expired, a bin that is no longer allocatable).
  • picking_create_batch holds an explicit selection to the batch size too: max, else the profile’s order count, else 20. A selection with more ready orders is answered 409 selection_too_large (with max and ready) and nothing is batched; it is never cut short.
  • Pick profiles (Elevate): pick_profiles_list, _preview, _create, _update, _archive, _restore and _reorder under /picking/profiles. picking_queue and picking_list_batches take profileId; orders_recent_tags (GET /orders/tags) suggests the tags and priorities to write conditions with.
  • picking_queue_exclusions gains stale: orders that wait for a fresh reservation.
  • orders_work_queue’s tag matches like an automation rule (case, surrounding spaces and accent composition do not matter) and is answered 409 tags_indexing for the minute after an update that re-derives the order tags.
  • A shipment rate’s amount and currency can be null: in shipments_get (shipment.rates[], shipment.chosenRate, shipment.purchaseRate) and in the shipments_list rows. null means a service on your own carrier contract that Sendcloud returned no price for, so the carrier’s invoice decides. A label bought on such a rate is recorded with cost: null and currency: null. Code that adds up amount must skip the null ones.

September 2026:

  • setup_guide_status is now setup_status (GET /setup), and answers four steps instead of six.
  • settings_set answers { ok: true, value } on success and { ok: false, reason: "warehouses_not_live", detail, warehouses, capped } when the Logistified switch is refused; inventoryWriterCutoverRequired is gone.
  • picking_queue_exclusions gains unmapped and takes warehouseId optionally.
  • The setting names openingStockSkipped and printerSetupSkipped no longer exist and are answered 400.
  • REST quickstart — a token, a list, a safe change and the errors, step by step
  • Connect an AI agent — the same operations through Claude, ChatGPT or another MCP client
  • API explorer — the interactive reference
  • Subscription plans — what the Enterprise tier includes
  • Integrations — how Floormode talks to the sibling apps
  • Webhooks — have Floormode call your system when something happens, and how to verify each message