{"openapi":"3.0.3","info":{"title":"Premium Apparel API","description":"**[Integration FAQ](/docs/faq)** — rate limits, GST treatment of prices, how `since` and `changed_at` behave, recovering removals, discontinued vs out of stock, read-only tokens, and testing without a sandbox. Read it before building a sync.\n\nSee the [Error reference](/docs/errors) for the full list of error codes, when they occur, and recommended client actions. The [Changelog](/docs/changelog) records every change to this surface and the versioning policy behind it.\n\n### Two things worth knowing up front\n\n- **Prices are exclusive of GST**, in and out, in both regions. The one exception is the no-token catalogue, which returns GST-inclusive RRP. See the [FAQ](/docs/faq) for how to send tax-inclusive prices on an order.\n- **There is no rate limit today** — nothing returns `429`. If one is introduced it will be announced on the [Changelog](/docs/changelog) first, and will use `429` with `Retry-After`.\n\n### AI assistants — MCP\n\nThis API also speaks the Model Context Protocol at `POST /mcp`, so an AI assistant can call it directly instead of you writing integration code. It is the same API: the same bearer token, the same data, the same permissions, and each tool runs the same code as the REST operation it mirrors — nothing is reachable over MCP that is not already reachable here.\n\nThe MCP surface is sales orders, the product catalogue, and updating existing products: `create_order`, `find_order_by_reference`, `get_order`, `update_order`, `list_products`, `get_stock`, `get_prices`, and `update_products` (which needs the products write permission on your account). Purchase orders, creating products and adding images have no MCP tool and remain REST-only. Catalogue reads over MCP are capped at roughly 256 KB, so filter them or poll with `since`; bulk export stays a REST job.\n\nConnection instructions and per-tool detail: [MCP guide](/docs/mcp).","version":"0.1.0"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"ErrorResponse":{"type":"object","required":["error","message"],"additionalProperties":false,"properties":{"error":{"type":"string","enum":["invalid_token","forbidden","not_found","duplicate_reference","conflict","validation_error","upstream_unavailable","upstream_error"]},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true}},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"paths":{"/health":{"get":{"summary":"Health check","tags":["Health"],"description":"Returns 200 when the service is ready to handle requests, 503 when it is not. Unauthenticated.","security":[],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ready"]},"upstream":{"type":"string","enum":["connected"]}},"required":["status","upstream"]}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["not_ready"]},"upstream":{"type":"string","enum":["unavailable"]}},"required":["status","upstream"]},"example":{"error":"upstream_unavailable","message":"Upstream service is temporarily unavailable"}}}}}}},"/v1/orders":{"post":{"summary":"Create order","tags":["Orders"],"description":"Creates a new order. The order_no is the integrator-supplied external reference; a duplicate value returns 409. Optionally include `contact_id` to place the order on behalf of that contact instead of the one tied to your token; this requires elevated permission and otherwise returns 403 (see the error reference).\n\nSend `app` (e.g. `Flyte`) to identify your application — it is recorded against the order. A `price` on a line item is the agreed unit price and overrides both the price book and the customer discount; omit it to be priced from the price book.\n\nA `description` on a line item replaces the product's own description on the order and packing slip — for example so a generic decoration or line code names the actual job. Omit it (or send it blank) to keep the product's description.\n\n**How line items become order lines.** An order line holds one product across all its sizes, at one price and with one description. Line items for the same product that send the same `description` and the same `price` (or neither) are combined into one order line, and a size sent twice is added up. Line items for the same product with a different `description` or `price` become separate order lines, so the same SKU can appear on several lines (for example one per decoration design).\n\n**`price` is read as exclusive of GST/VAT**, matching what `/v1/prices` returns. To send tax-inclusive prices instead, set `taxes_included: true` and `tax_lines[0].rate` — a fraction, not a percentage (`0.15` for 15% NZ GST, `0.10` for 10% AU GST). The order stores the tax-exclusive figure either way, and both fields are ignored when no line carries a price.\n\nCreating an order requires a token permitted to write orders; a read-only token is refused with 403. There is no sandbox and no dry-run — see the [Integration FAQ](/docs/faq) for how to test order creation safely.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["order_no","line_items"],"properties":{"order_no":{"type":"string","minLength":1},"app":{"type":"string"},"contact_id":{"type":"string"},"shipping_method":{"type":"string"},"delivery_instructions":{"type":"string"},"comments":{"type":"string"},"taxes_included":{"type":"boolean"},"tax_lines":{"type":"array","items":{"type":"object","required":["rate"],"properties":{"rate":{"type":"number","minimum":0}},"additionalProperties":false}},"customer":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}},"additionalProperties":false},"shipping_address":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"company":{"type":"string"},"address1":{"type":"string"},"address2":{"type":"string"},"city":{"type":"string"},"province":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"}},"additionalProperties":false},"line_items":{"type":"array","minItems":1,"items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string","minLength":1},"quantity":{"type":"integer","not":{"enum":[0]},"description":"Units ordered. Must not be 0. A negative quantity is accepted only for a discount product (RETDIS): send quantity -1 with a positive `price` to take that amount off the order."},"price":{"type":"number","minimum":0},"description":{"type":"string","maxLength":255}},"additionalProperties":false}}},"additionalProperties":false,"example":{"order_no":"monday-item-12345","app":"Flyte","contact_id":"EXAMPLE","shipping_method":"STD","delivery_instructions":"Leave at reception","comments":"Gift wrap requested","taxes_included":false,"tax_lines":[{"rate":0.15}],"customer":{"first_name":"Jane","last_name":"Doe"},"shipping_address":{"name":"Jane Doe","company":"Acme Pty Ltd","address1":"123 Main St","address2":"Unit 4","city":"Sydney","province":"NSW","zip":"2000","country":"AU","phone":"0412345678"},"line_items":[{"sku":"WIDGET-001","quantity":2,"price":12.5},{"sku":"GADGET-XL","quantity":1}]}}}},"required":true},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","required":["our_ref","order_no","status","created_at"],"properties":{"our_ref":{"type":"string"},"order_no":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"on_hold":{"type":"boolean","description":"True when the order was created on Hold and must be released by staff before it is picked. Tokens set up to create held orders always get true; false for such a token means the hold could not be applied and the order is live."}},"additionalProperties":true,"example":{"our_ref":"40544","order_no":"monday-item-12345","status":"received","created_at":"2026-04-20T09:12:34Z","on_hold":false}}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","message":"You do not have permission to perform that action"}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"duplicate_reference","message":"Order with this order_no already exists"}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Request body failed validation","details":{"path":"/line_items/0/sku","issue":"required"}}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_error","message":"Upstream system returned an unexpected response"}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_unavailable","message":"Upstream service is temporarily unavailable"}}}}}},"get":{"summary":"Find order by external reference","tags":["Orders"],"description":"Returns the order whose `order_no` matches the supplied query value. Inaccessible or non-existent references return 404 with a generic message to prevent reference-space enumeration.\n\nThe response is the order header plus `line_items` (`sku`, `quantity` ordered, `quantity_shipped`, and `price` when the line holds one). Dispatch details: `ship_via`, `courier_ticket_no`, `date_shipped` (often empty — rely on `shipments` and an `Invoice` status instead), and `shipments`, one `{ carrier, tracking_number }` per courier consignment. `on_hold` is true while the order is held and cannot be picked. `paid` is true when the order is marked as paid.","parameters":[{"schema":{"type":"string","minLength":1},"in":"query","name":"order_no","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","required":["our_ref","order_no","status","created_at"],"properties":{"our_ref":{"type":"string"},"order_no":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"shipping_method":{"type":"string"},"shipping_address":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"company":{"type":"string"},"address1":{"type":"string"},"address2":{"type":"string"},"city":{"type":"string"},"province":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"}},"additionalProperties":false},"customer":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}},"additionalProperties":false},"line_items":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer"},"price":{"type":"number"},"quantity_shipped":{"type":"integer"}},"additionalProperties":false}},"ship_via":{"type":"string","nullable":true,"description":"The carrier or service chosen for the order."},"date_shipped":{"type":"string","nullable":true,"description":"`YYYY-MM-DD` when the order was marked dispatched in GW, else null. Often not filled in: treat `shipments` and an `Invoice` status as the dispatch signal."},"courier_ticket_no":{"type":"string","nullable":true,"description":"The courier ticket held on the order, if any."},"on_hold":{"type":"boolean","description":"True while the order is on Hold and cannot be picked."},"paid":{"type":"boolean","description":"True when the order is marked as paid."},"shipments":{"type":"array","description":"One entry per courier consignment, e.g. for tracking. Empty until dispatched.","items":{"type":"object","required":["tracking_number"],"properties":{"carrier":{"type":"string","nullable":true},"tracking_number":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false,"example":{"our_ref":"40544","order_no":"monday-item-12345","status":"received","created_at":"2026-04-19T02:14:07Z","updated_at":"2026-04-19T02:14:07Z","shipping_method":"STD","shipping_address":{"name":"Jane Doe","address1":"123 Main St","city":"Sydney","province":"NSW","zip":"2000","country":"AU"},"customer":{"first_name":"Jane","last_name":"Doe"},"line_items":[{"sku":"WIDGET-001","quantity":2,"quantity_shipped":2},{"sku":"GADGET-XL","quantity":1,"quantity_shipped":0}],"ship_via":"NZ Post","date_shipped":null,"courier_ticket_no":"00000000000000000000","on_hold":false,"paid":false,"shipments":[{"carrier":"NZ Post","tracking_number":"00000000000000000000"}]}}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","message":"Order not found"}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Request body failed validation","details":{"path":"/line_items/0/sku","issue":"required"}}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_error","message":"Upstream system returned an unexpected response"}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_unavailable","message":"Upstream service is temporarily unavailable"}}}}}}},"/v1/orders/{our_ref}":{"patch":{"summary":"Update order header","tags":["Orders"],"description":"Amends the header of an order that already exists: the Attention contact, the delivery address, the required-by date, and the internal message.\n\nOnly the fields you send are changed — an omitted field is left alone, and a field sent as `\"\"` is cleared. Two exceptions worth reading before you call it:\n\n- **`comments_append` appends**, on its own line, and never overwrites. Orders created by the CSV importer already carry the customer's standing notes in that field. Sending text the message already contains does nothing, so retrying a timed-out call is safe.\n- **`shipping_address` replaces the whole delivery address.** Anything you leave out is cleared, so send every line you want kept; `address1` and `city` are required whenever it is present.\n\nOnly orders still in the Order state can be amended; once converted to an invoice the call returns 409. The response names the fields that actually changed, so a field sent with the value it already held comes back absent rather than as an error.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["app"],"properties":{"app":{"type":"string","minLength":1},"attention":{"type":"string","description":"The contact the order is for. Wins over the name/company rule that `shipping_address` applies. Send `\"\"` to clear it."},"shipping_address":{"type":"object","required":["address1","city"],"properties":{"name":{"type":"string"},"phone":{"type":"string"},"company":{"type":"string"},"address1":{"type":"string"},"address2":{"type":"string"},"city":{"type":"string"},"province":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"}},"additionalProperties":false,"description":"Replaces the WHOLE delivery address — anything not sent is cleared, so send every line you want kept. `company` names the delivery party; when only `name` is given it is used for that instead, and when both are given `name` becomes the Attention contact unless the `attention` field overrides it."},"required_by":{"type":"string","pattern":"^(\\d{4}-\\d{2}-\\d{2})?$","description":"Required-by date as `YYYY-MM-DD`, or `\"\"` to clear it. Also pushed down onto the order's lines, as changing it in GW does."},"comments_append":{"type":"string","description":"Text to APPEND to the order's internal message on its own line. Never overwrites: importer-created orders hold the customer's standing notes there. Sending text the message already contains does nothing."}},"additionalProperties":false,"example":{"app":"Flyte","attention":"Jane Example","required_by":"2026-09-25","comments_append":"PACKING SLIP 2","shipping_address":{"name":"Jane Example","company":"Example Community Trust","address1":"1 Example Street","city":"Exampletown","zip":"9999","country":"NZ"}}}}},"required":true},"parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"our_ref","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","required":["our_ref","updated"],"properties":{"our_ref":{"type":"string"},"updated":{"type":"array","items":{"type":"string"}}},"additionalProperties":true,"example":{"our_ref":"0999001","updated":["attention","shipping_address","comments_append"]}}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"forbidden","message":"You do not have permission to perform that action"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","message":"Order not found"}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"duplicate_reference","message":"Order with this order_no already exists"}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Request body failed validation","details":{"path":"/line_items/0/sku","issue":"required"}}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_error","message":"Upstream system returned an unexpected response"}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_unavailable","message":"Upstream service is temporarily unavailable"}}}}}}},"/v1/orders/{id}":{"get":{"summary":"Get order by id","tags":["Orders"],"description":"Returns the order identified by the internal id (`our_ref`). Inaccessible or non-existent ids return 404 with a generic message to prevent enumeration.\n\nThe response is the order header plus `line_items` (`sku`, `quantity` ordered, `quantity_shipped`, and `price` when the line holds one). Dispatch details: `ship_via`, `courier_ticket_no`, `date_shipped` (often empty — rely on `shipments` and an `Invoice` status instead), and `shipments`, one `{ carrier, tracking_number }` per courier consignment. `on_hold` is true while the order is held and cannot be picked. `paid` is true when the order is marked as paid.","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","required":["our_ref","order_no","status","created_at"],"properties":{"our_ref":{"type":"string"},"order_no":{"type":"string"},"status":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"},"shipping_method":{"type":"string"},"shipping_address":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"company":{"type":"string"},"address1":{"type":"string"},"address2":{"type":"string"},"city":{"type":"string"},"province":{"type":"string"},"zip":{"type":"string"},"country":{"type":"string"}},"additionalProperties":false},"customer":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"type":"string"}},"additionalProperties":false},"line_items":{"type":"array","items":{"type":"object","required":["sku","quantity"],"properties":{"sku":{"type":"string"},"quantity":{"type":"integer"},"price":{"type":"number"},"quantity_shipped":{"type":"integer"}},"additionalProperties":false}},"ship_via":{"type":"string","nullable":true,"description":"The carrier or service chosen for the order."},"date_shipped":{"type":"string","nullable":true,"description":"`YYYY-MM-DD` when the order was marked dispatched in GW, else null. Often not filled in: treat `shipments` and an `Invoice` status as the dispatch signal."},"courier_ticket_no":{"type":"string","nullable":true,"description":"The courier ticket held on the order, if any."},"on_hold":{"type":"boolean","description":"True while the order is on Hold and cannot be picked."},"paid":{"type":"boolean","description":"True when the order is marked as paid."},"shipments":{"type":"array","description":"One entry per courier consignment, e.g. for tracking. Empty until dispatched.","items":{"type":"object","required":["tracking_number"],"properties":{"carrier":{"type":"string","nullable":true},"tracking_number":{"type":"string"}},"additionalProperties":false}}},"additionalProperties":false,"example":{"our_ref":"40544","order_no":"monday-item-12345","status":"received","created_at":"2026-04-19T02:14:07Z","updated_at":"2026-04-19T02:14:07Z","shipping_method":"STD","shipping_address":{"name":"Jane Doe","address1":"123 Main St","city":"Sydney","province":"NSW","zip":"2000","country":"AU"},"customer":{"first_name":"Jane","last_name":"Doe"},"line_items":[{"sku":"WIDGET-001","quantity":2,"quantity_shipped":2},{"sku":"GADGET-XL","quantity":1,"quantity_shipped":0}],"ship_via":"NZ Post","date_shipped":null,"courier_ticket_no":"00000000000000000000","on_hold":false,"paid":false,"shipments":[{"carrier":"NZ Post","tracking_number":"00000000000000000000"}]}}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"not_found","message":"Order not found"}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Request body failed validation","details":{"path":"/line_items/0/sku","issue":"required"}}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_error","message":"Upstream system returned an unexpected response"}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"upstream_unavailable","message":"Upstream service is temporarily unavailable"}}}}}}},"/v1/products":{"get":{"summary":"Product catalogue","tags":["Products"],"description":"Returns the product catalogue. The `Authorization: Bearer <key>` header is a **customer token** that selects that customer's pricing; without a token, the default catalogue with RRP pricing is returned. The catalogue region (NZ/AU) is selected by the request Host.\n\n**Prices are exclusive of GST/VAT** — except on the no-token catalogue, whose RRP figures are GST-inclusive display prices.\n\nEach size also carries `barcode` (a string: leading zeros matter), `units_per_carton`, and `rrp` (the GST-inclusive recommended retail price); each style carries `packing_units` for freight banding. Each is `null` where Premium Apparel holds no value. See the [Integration FAQ](/docs/faq).\n\n**Incremental sync.** Every response carries a `Last-Modified` header — the time the data was last updated. Pass that value back as `?since=` and you receive only the products that changed since it, wrapped as `{ as_of, as_of_epoch_ms, count, products }`. A timestamp only moves on a real change; nothing is restamped on a schedule. But here **changed means anything at all changed — stock and price included**, so in practice a large share of the catalogue is returned on every poll because stock moves constantly. For stock or price polling use `/v1/stock` and `/v1/prices`, which track those dimensions separately and return a fraction of the data; reserve `/v1/products?since=` for a slow content sync. The same response carries `removed_styles` — products that no longer exist, which you must process to avoid keeping discontinued products, since a product missing from `products` only means it did not change. Removals are never expired, so `since=1` returns the full catalogue plus every removal ever recorded. Without `since` the full catalogue is returned. Worked numbers and polling guidance: [Integration FAQ](/docs/faq).\n\n**Error shape:** this endpoint uses the legacy `{ \"error\": true, \"error_reason\": \"...\" }` envelope (not the canonical `{ error, message }`), preserved for existing clients. The deprecated `?style=` parameter is no longer supported and returns a 400.","parameters":[{"schema":{"type":"string","minLength":1,"example":"2026-08-18T13:50:52Z"},"in":"query","name":"since","required":false,"description":"Only return entries changed after this time. Accepts an ISO 8601 timestamp (`2026-08-18T13:50:52Z`), an HTTP date as returned in `Last-Modified` (`Tue, 18 Aug 2026 13:50:52 GMT`), or a Unix epoch in seconds or milliseconds. Pass back the `as_of`, `as_of_epoch_ms`, or `Last-Modified` from your previous response — all three name the same instant — to receive exactly what has changed since it. The comparison is strictly greater-than, so replaying the same value returns nothing rather than repeating the last batch."}],"security":[],"responses":{"200":{"description":"The catalogue JSON — a free-form product list with pricing. With `?since=`, the same products wrapped as `{ as_of, as_of_epoch_ms, count, products }` and filtered to those that changed.","content":{"application/json":{"schema":{"description":"The catalogue JSON — a free-form product list with pricing. With `?since=`, the same products wrapped as `{ as_of, as_of_epoch_ms, count, products }` and filtered to those that changed.","type":"object","additionalProperties":true}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"ProductsError#"}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"ProductsError#"},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"ProductsError#"},"example":{"error":"upstream_unavailable","message":"Upstream service is temporarily unavailable"}}}}}}},"/v1/stock":{"get":{"summary":"Stock levels","tags":["Products"],"description":"Returns the stock level for each size-level SKU.\n\nFilter with `product_id` (the product code, e.g. `A101CVC2BL`), `style` (e.g. `101CVC`, returning every colour and size) or `sku` (an exact size-level SKU). Repeat a parameter or comma-separate to pass several; combining different parameters widens the result rather than narrowing it. Omit all three to receive everything.\n\nAdd `since` to receive only what has changed. Every response carries `as_of` — the time the data was last updated — and passing it back as `since` on your next call returns exactly what changed in between. `since` combines with the filters, so you can poll changes within a single style.\n\nA `since` response also carries `removed`: SKUs that no longer exist. Read it — a SKU missing from `items` only means it did not change, so removals are invisible unless you process `removed`. Tombstones are never expired, so `since=1` returns a full snapshot **plus** every removal ever recorded — that is how you recover after losing your cursor.\n\n**Discontinued vs out of stock.** There is no status flag, and none is needed: a SKU that is out of stock is *present* with `stock: 0`, while a withdrawn SKU is *absent* and appears in `removed`. `stock` is floored at zero and is never negative.\n\n`Authorization: Bearer <key>` is your **customer token**, the same one `/v1/products` takes.\n\n**Host:** call this on `api.premiumapparel.co.nz` (NZ) or `api.premiumapparel.com.au` (AU) — the host selects the region.\n\nAt most 200 filter values across `product_id`, `style` and `sku` combined; beyond that the request is refused with `422`. More on polling cadence and limits in the [Integration FAQ](/docs/faq).","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":"A101CVC2BL"},"in":"query","name":"product_id","required":false,"description":"Product code(s) — the SKU without its size suffix, e.g. `A101CVC2BL`. Repeat the parameter or comma-separate for several."},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":"101CVC"},"in":"query","name":"style","required":false,"description":"Style code(s), e.g. `101CVC`. Returns every colour and size of the style. Repeat the parameter or comma-separate for several."},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":"A101CVC2BL-LS"},"in":"query","name":"sku","required":false,"description":"Exact size-level SKU(s), e.g. `A101CVC2BL-LS`. Repeat the parameter or comma-separate for several."},{"schema":{"type":"string","minLength":1,"example":"2026-08-18T13:50:52Z"},"in":"query","name":"since","required":false,"description":"Only return entries changed after this time. Accepts an ISO 8601 timestamp (`2026-08-18T13:50:52Z`), an HTTP date as returned in `Last-Modified` (`Tue, 18 Aug 2026 13:50:52 GMT`), or a Unix epoch in seconds or milliseconds. Pass back the `as_of`, `as_of_epoch_ms`, or `Last-Modified` from your previous response — all three name the same instant — to receive exactly what has changed since it. The comparison is strictly greater-than, so replaying the same value returns nothing rather than repeating the last batch."}],"security":[],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","required":["as_of","as_of_epoch_ms","count","items"],"properties":{"as_of":{"type":"string","description":"When the data was last updated. Pass it back as `since` on your next poll.","example":"2026-08-18T13:50:52.000Z"},"as_of_epoch_ms":{"type":"integer","description":"The same instant as `as_of`, in Unix epoch milliseconds.","example":1787017852000},"count":{"type":"integer","description":"Number of entries in `items` after filtering.","example":5},"items":{"type":"array","items":{"type":"object","required":["sku","product_id","style","colour","size","stock","changed_at"],"properties":{"sku":{"type":"string","example":"A101CVC2BL-LS"},"product_id":{"type":"string","description":"The product code — the SKU without its size suffix.","example":"A101CVC2BL"},"style":{"type":"string","example":"101CVC"},"colour":{"type":"string","description":"Colour code.","example":"BL"},"colour_description":{"type":"string","example":"Black"},"size":{"type":"string","example":"LS"},"stock":{"type":"integer","description":"Units available.","example":1},"changed_at":{"type":"string","description":"When this SKU's stock last changed. Never later than `as_of`.","example":"2026-08-18T13:50:52.000Z"}},"additionalProperties":false}},"removed":{"type":"array","description":"SKUs that no longer exist, removed since `since`. Only present when `since` is supplied, and subject to the same filters as `items`. Delete these from your local copy — they will not appear in `items` again unless they are reinstated.","items":{"type":"object","required":["sku","product_id","style","removed_at"],"properties":{"sku":{"type":"string","example":"A101CVC2BL-LS"},"product_id":{"type":"string","example":"A101CVC2BL"},"style":{"type":"string","example":"101CVC"},"removed_at":{"type":"string","example":"2026-08-18T13:50:52.000Z"}},"additionalProperties":false}}},"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Request body failed validation","details":{"path":"/line_items/0/sku","issue":"required"}}}}}}}},"/v1/prices":{"get":{"summary":"Prices","tags":["Products"],"description":"Returns the unit price for each size-level SKU.\n\n**Prices are exclusive of GST/VAT.** This holds in both regions, and for the prices you send back on an order.\n\n**Pricing is per customer.** The prices returned are those of the token holder; without a token you receive RRP — which is the one **GST-inclusive** figure this API serves, and is a display price rather than one you can transact at. Prices are decimal **strings** and are never rounded or reformatted.\n\nFilter with `product_id` (the product code, e.g. `A101CVC2BL`), `style` (e.g. `101CVC`, returning every colour and size) or `sku` (an exact size-level SKU). Repeat a parameter or comma-separate to pass several; combining different parameters widens the result rather than narrowing it. Omit all three to receive everything.\n\nAdd `since` to receive only what has changed. Every response carries `as_of` — the time the data was last updated — and passing it back as `since` on your next call returns exactly what changed in between. `since` combines with the filters, so you can poll changes within a single style.\n\nA `since` response also carries `removed`: SKUs that no longer exist. Read it — a SKU missing from `items` only means it did not change, so removals are invisible unless you process `removed`. Tombstones are never expired, so `since=1` returns a full snapshot **plus** every removal ever recorded — that is how you recover after losing your cursor.\n\n**Discontinued vs out of stock.** There is no status flag, and none is needed: a SKU that is out of stock is *present* with `stock: 0`, while a withdrawn SKU is *absent* and appears in `removed`. `stock` is floored at zero and is never negative.\n\n`Authorization: Bearer <key>` is your **customer token**, the same one `/v1/products` takes.\n\n**Host:** call this on `api.premiumapparel.co.nz` (NZ) or `api.premiumapparel.com.au` (AU) — the host selects the region.\n\nAt most 200 filter values across `product_id`, `style` and `sku` combined; beyond that the request is refused with `422`. More on polling cadence and limits in the [Integration FAQ](/docs/faq).","parameters":[{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":"A101CVC2BL"},"in":"query","name":"product_id","required":false,"description":"Product code(s) — the SKU without its size suffix, e.g. `A101CVC2BL`. Repeat the parameter or comma-separate for several."},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":"101CVC"},"in":"query","name":"style","required":false,"description":"Style code(s), e.g. `101CVC`. Returns every colour and size of the style. Repeat the parameter or comma-separate for several."},{"schema":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"example":"A101CVC2BL-LS"},"in":"query","name":"sku","required":false,"description":"Exact size-level SKU(s), e.g. `A101CVC2BL-LS`. Repeat the parameter or comma-separate for several."},{"schema":{"type":"string","minLength":1,"example":"2026-08-18T13:50:52Z"},"in":"query","name":"since","required":false,"description":"Only return entries changed after this time. Accepts an ISO 8601 timestamp (`2026-08-18T13:50:52Z`), an HTTP date as returned in `Last-Modified` (`Tue, 18 Aug 2026 13:50:52 GMT`), or a Unix epoch in seconds or milliseconds. Pass back the `as_of`, `as_of_epoch_ms`, or `Last-Modified` from your previous response — all three name the same instant — to receive exactly what has changed since it. The comparison is strictly greater-than, so replaying the same value returns nothing rather than repeating the last batch."}],"security":[],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","required":["as_of","as_of_epoch_ms","count","items"],"properties":{"as_of":{"type":"string","description":"When the data was last updated. Pass it back as `since` on your next poll.","example":"2026-08-18T13:50:52.000Z"},"as_of_epoch_ms":{"type":"integer","description":"The same instant as `as_of`, in Unix epoch milliseconds.","example":1787017852000},"count":{"type":"integer","description":"Number of entries in `items` after filtering.","example":5},"items":{"type":"array","items":{"type":"object","required":["sku","product_id","style","colour","size","price","changed_at"],"properties":{"sku":{"type":"string","example":"A101CVC2BL-LS"},"product_id":{"type":"string","description":"The product code — the SKU without its size suffix.","example":"A101CVC2BL"},"style":{"type":"string","example":"101CVC"},"colour":{"type":"string","description":"Colour code.","example":"BL"},"colour_description":{"type":"string","example":"Black"},"size":{"type":"string","example":"LS"},"price":{"type":"string","description":"Unit price for the authenticated customer, or RRP when no token was sent. A decimal string, not a number, so it is never rounded or reformatted.","example":"24.95"},"changed_at":{"type":"string","description":"When this SKU's price last changed. Never later than `as_of`.","example":"2026-08-18T13:50:52.000Z"}},"additionalProperties":false}},"removed":{"type":"array","description":"SKUs that no longer exist, removed since `since`. Only present when `since` is supplied, and subject to the same filters as `items`. Delete these from your local copy — they will not appear in `items` again unless they are reinstated.","items":{"type":"object","required":["sku","product_id","style","removed_at"],"properties":{"sku":{"type":"string","example":"A101CVC2BL-LS"},"product_id":{"type":"string","example":"A101CVC2BL"},"style":{"type":"string","example":"101CVC"},"removed_at":{"type":"string","example":"2026-08-18T13:50:52.000Z"}},"additionalProperties":false}}},"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"invalid_token","message":"Bearer token is invalid or expired"}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":"validation_error","message":"Request body failed validation","details":{"path":"/line_items/0/sku","issue":"required"}}}}}}}},"/v1/openapi.json":{"get":{"summary":"OpenAPI 3.0.3 specification","tags":["Meta"],"description":"Returns the OpenAPI specification for the standard API surface (product catalogue and sales orders). Generated from Fastify route schemas via @fastify/swagger. Public.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/docs/errors":{"get":{"summary":"Error reference (rendered HTML)","tags":["Meta"],"description":"Human-readable error reference listing every error code the API can emit, when it occurs, and the recommended client action. Rendered from `docs/api-errors.md`. Public.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/docs/mcp":{"get":{"summary":"MCP connection guide (rendered HTML)","tags":["Meta"],"description":"How to connect an AI assistant to this API over the Model Context Protocol: endpoint, authentication, the available tools, and their limits. Rendered from `docs/mcp.md`. Public.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/docs/faq":{"get":{"summary":"Integration FAQ (rendered HTML)","tags":["Meta"],"description":"Answers to the questions integrators ask before they build: rate limits, GST treatment of prices, what `since` and `changed_at` actually track, recovering removals after losing a watermark, telling discontinued apart from out of stock, read-only tokens, and testing without a sandbox. Rendered from `docs/integration-faq.md`. Public.","security":[],"responses":{"200":{"description":"Default Response"}}}},"/docs/changelog":{"get":{"summary":"Changelog and versioning policy (rendered HTML)","tags":["Meta"],"description":"Every change to the API surface, newest first, with the date it reached production — and the versioning policy: what counts as a breaking change, the 90-day notice period, and which additive changes ship without notice. Rendered from `docs/changelog.md`. Public.","security":[],"responses":{"200":{"description":"Default Response"}}}}},"security":[{"bearerAuth":[]}],"tags":[{"name":"Health","description":"Liveness and readiness probes (unauthenticated)."},{"name":"Orders","description":"Order create, lookup, and find operations."},{"name":"Products","description":"Product catalogue. A customer token returns that customer's pricing; no token returns RRP."},{"name":"Meta","description":"API metadata (OpenAPI spec, docs)."}]}