Premium Apparel API — Integration FAQ
Answers to the questions integrators ask before they build. Everything below describes the API as it behaves today. Where something is not available, it says so plainly rather than implying it is coming.
API reference · Error reference · Changelog · MCP guide
Rate limits
There is no enforced rate limit. No endpoint returns 429, and no Retry-After or
RateLimit-* headers are sent.
If a limit is ever introduced it will be announced on the changelog
before it takes effect, and it will behave conventionally: HTTP 429 with a
Retry-After header giving the number of seconds to wait. Honour Retry-After on a
429 in your client now and you will not need to change anything later.
Poll sensibly rather than hard
The catalogue is not live data. FileMaker rewrites each customer's catalogue file on a
rolling cycle that works through every provisioned token and takes roughly an hour, so a
given token's data changes about once an hour. Polling faster returns the same as_of
and — with since — an empty result set.
- Poll
/v1/stockand/v1/priceswith?since=. These return only what changed, typically a few KB. - Do not poll
GET /v1/productswithoutsinceon a schedule: that is the full catalogue, about 1.8 MB per call. - Every catalogue response carries
Last-Modified, which is the same instant asas_of. If it has not moved, nothing has changed.
The limits that do exist
| Limit | Value | What happens when you exceed it |
|---|---|---|
Filter values on /v1/stock and /v1/prices |
200 across product_id + style + sku combined |
422 validation_error naming the count you sent |
Response size over MCP (POST /mcp) |
~256 KB | The tool result is truncated with a notice. Filter, or use since. REST has no such cap. |
| Upstream call timeout | 5 seconds | 503 upstream_unavailable. Retry with backoff — see the error reference. |
Upstream call timeout, POST /v1/orders |
30 seconds | A large order is given longer. If it still runs out, the API looks for the order by order_no before answering: you get the real 201 if it was created, otherwise 503 with details.reason: "outcome_unknown" — see the error reference. |
Are prices exclusive of GST?
Yes. Every price the API accepts or returns for a token holder is exclusive of GST/VAT, in both regions.
That covers:
priceon each size inGET /v1/productspriceinGET /v1/pricesline_items[].priceyou send toPOST /v1/ordersandPUT /v1/orders/{our_ref}/line-items
Prices are decimal strings in the catalogue responses and are never rounded or reformatted on the way out.
Sending tax-inclusive prices on an order
If your system holds tax-inclusive prices and you would rather not convert them, say so explicitly and the conversion is done for you:
{
"order_no": "OMS-12345",
"taxes_included": true,
"tax_lines": [{ "rate": 0.15 }],
"line_items": [{ "sku": "A1022AR-LS", "quantity": 2, "price": 10.81 }]
}
rate is 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. Both fields are ignored when
no line carries a price.
The one exception: the no-token RRP catalogue
Calling the catalogue endpoints without a bearer token returns the default catalogue,
whose price is the recommended retail price, and RRP is GST-inclusive by Premium
Apparel's own convention. It is a display price, not a price you can transact at.
Authenticate. A token returns your contracted pricing, exclusive of GST, which is the only pricing an order will ever be accepted at.
Can a response carry the account code it was priced for?
Not today. Catalogue responses identify no account. Your bearer token selects a pre-generated catalogue file, and the account behind that token is resolved inside FileMaker at generation time — the file itself carries no account identifier for the API to echo back.
In practice this is rarely a problem, because the mapping is one-to-one and stable: one
token, one account, one price list. If your system genuinely needs the account code in
the payload — for example you hold several Premium Apparel accounts and route responses
by code rather than by which token you used — tell your account contact. It is a small
change to the file generator and it would surface as a top-level field on
/v1/products, /v1/stock and /v1/prices. It is not currently scheduled.
Orders are not affected: an order is always attributed server-side to the account the token resolves to, whether or not you name one.
Incremental sync: what changed_at actually tracks
changed_at already moves only on a real change. Nothing stamps the whole catalogue
on a schedule.
The catalogue files carry no timestamps of their own, so the API derives change detection by diffing each regeneration against the previous one's recorded values and stamping only the entries whose value actually differs. An unchanged entry carries its old timestamp forward untouched, across regenerations, deploys and restarts.
What differs between the endpoints is what counts as a change:
| Endpoint | since returns |
Because |
|---|---|---|
/v1/stock |
SKUs whose stock number changed | tracked per SKU, stock only |
/v1/prices |
SKUs whose price changed | tracked per SKU, price only |
/v1/products |
styles where anything at all changed — title, images, description, size chart, and stock, and price | you are syncing whole product records, so any difference counts |
That last row is why /v1/products?since= feels like it returns everything. Stock moves
constantly, and one SKU moving makes its whole style "changed". Measured on a live
account, one regeneration of a 441-style / 12,376-SKU catalogue moved:
- 1,232 SKUs' stock
- 10 SKUs' prices
- and therefore 167 of 441 styles on
/v1/products?since=
The fix is to stop asking /v1/products for the volatile dimensions:
- Stock → poll
/v1/stock?since= - Prices → poll
/v1/prices?since= - Product content (new styles, retitles, new images, description edits) → poll
/v1/products?since=on a much slower cadence, daily or weekly. It will still include styles that only moved stock, but you are no longer paying for that every few minutes.
There is currently no way to ask /v1/products?since= for descriptive changes only. If
that would materially help your integration, raise it with your account contact.
Cursor mechanics
Pass back exactly what you were given. Every response carries as_of (ISO 8601),
as_of_epoch_ms, and the same instant as the Last-Modified header; since accepts an
ISO 8601 timestamp or a Unix epoch in seconds or milliseconds. The comparison is strictly
greater-than, so replaying the previous as_of returns nothing rather than the batch you
already have. A value we cannot parse returns 422 rather than being silently ignored.
Recovering removals after losing a since watermark
You do not need a special reconcile path. Removals are never forgotten.
Withdrawn SKUs and styles are kept as tombstones and are never expired, so a since
value from arbitrarily far back returns the complete removal history along with a full
snapshot:
GET /v1/stock?since=1
GET /v1/products?since=1
/v1/stockand/v1/pricesreturn every current SKU initems, plus every removal ever recorded inremoved— each entry carryingsku,product_id,styleandremoved_at./v1/productsreturns every current product inproducts, plusremoved_styles.
Two things to know:
- The tombstone list begins when the API started change-indexing your catalogue, not at the beginning of time. Anything withdrawn before that was already absent from the first snapshot, so it never produced a tombstone.
- A plain full read is always authoritative.
GET /v1/productswith nosinceis the complete current catalogue — anything of yours that is not in it no longer exists, tombstone or no tombstone. Use that as the belt-and-braces reconcile if you prefer not to depend on tombstones at all.
removed and removed_styles are only present on a since request. Without since
the response is the current state, so absence already says it.
Read them. A SKU missing from a since response means "did not change", not
"withdrawn". If you ignore removed, discontinued items stay in your system forever.
Telling discontinued apart from out of stock
There is no active or status flag on a style, colour or SKU — and none is needed,
because the two states are already unambiguous:
| State | How it appears |
|---|---|
| Out of stock | The SKU is present in the catalogue with stock: 0 |
| Withdrawn / discontinued | The SKU is absent from the catalogue, and appears in removed (or its style in removed_styles) on a since request |
stock is the sellable quantity and is floored at zero — it is never negative, so 0
means exactly "none available", not "oversold".
The grains work the way you would expect. Discontinue one colour and that colour's SKUs
are tombstoned in removed while the style stays live. Discontinue the whole style and
it appears in removed_styles.
A worked example from live data — one colour of style 1301 was withdrawn, giving six
SKU tombstones and no style tombstone:
{
"sku": "UCP220L URBAN COLLAB LADIES POLO - NAVY-S",
"product_id": "UCP220L URBAN COLLAB LADIES POLO - NAVY",
"style": "1301",
"removed_at": "2026-09-14T21:50:24.000Z"
}
One caveat worth building for: "removed" means "no longer offered through this catalogue", which is usually a discontinuation but can also mean a product was deliberately excluded from the API feed. Either way you should stop offering it. If a product is reinstated its tombstone is dropped and it reappears as a normal change, so a removal is not permanent.
Barcodes, carton quantity, packing units and RRP
Each size in /v1/products carries:
barcode: the GTIN (EAN-13 where held) for that size, as a string so leading zeros survive, ornullwhere none is held.units_per_carton: units in an outer carton, ornull.rrp: the recommended retail price, GST-inclusive, the same figure the no-token catalogue publishes as itsprice. It is the one price in a token catalogue that includes GST.
Each style carries packing_units: the packing-unit figure courier freight bands are
worked out from (a tee is about 5, a hoodie about 25), or null where the style has none.
It is not a weight, and has no unit.
Coverage is not universal: a value that is not held is null, never guessed. Weight,
unit dimensions and volume are held but not published.
Dispatch information on order reads
GET /v1/orders/{our_ref} and GET /v1/orders?order_no= return dispatch details with
the order:
shipments: one{ carrier, tracking_number }per courier consignment (NZ Couriers or NZ Post). Empty until the order is dispatched. Use this for tracking.courier_ticket_no: the single ticket held on the order itself, ornull.ship_via: the carrier or service chosen for the order, ornull. It is set when the order is entered, so it does not mean the order has gone.date_shipped:YYYY-MM-DDwhen the order was marked dispatched, ornull. Often not filled in, so do not rely on it: anInvoicestatus plus an entry inshipmentsis the dependable sign that an order has gone.on_hold:truewhile the order is on Hold and cannot be picked.- On each line,
quantity_shipped: units already delivered.quantityminusquantity_shippedis what is still outstanding.
Can I get a read-only token?
Yes — this already works. Permissions are set per token, so a token can be issued that reads the catalogue and cannot create anything.
Ask your account contact for a read-only token. What you get:
| Operation | Read-only token |
|---|---|
GET /v1/products, /v1/stock, /v1/prices |
Allowed |
GET /v1/orders, GET /v1/orders/{id} |
Allowed, limited to your own orders |
POST /v1/orders |
403 forbidden |
PUT /v1/orders/{our_ref}/line-items |
403 forbidden |
| Purchase orders, product create/update, image upload | 403 forbidden |
The refusal happens in Premium Apparel's own system, not in a middleware check that could
be bypassed, and it is a clean 403 with the standard error envelope — see the
error reference.
A practical note: run your catalogue sync on the read-only token and keep the order-writing token separate and narrowly deployed. A catalogue sync that cannot place an order is one fewer way for a bug to become an order.
Is there a sandbox or test account?
No. There is no sandbox or test environment — api.premiumapparel.co.nz and
api.premiumapparel.com.au are production, and there is no staging equivalent.
That is less limiting than it sounds, because the two halves of the API carry very different risk.
Reads are completely safe. GET /v1/products, /v1/stock, /v1/prices and the order
reads are side-effect free. Develop against them freely, at whatever volume you like —
you cannot break anything and there is nothing to clean up. Start without a token at all
if you only want to see the catalogue shape: you get RRP pricing (GST-inclusive, see
above) and the identical structure.
POST /v1/orders creates a real order. There is no dry-run flag. To test order
creation:
- Ask your account contact to nominate a test account for you — a real account, set up knowing that orders placed against it are tests, so they can be cancelled rather than picked and shipped.
- Prefix
order_nodistinctively, for exampleTEST-OMS-0001, so test orders are obvious on both sides. - Tell your contact when you are testing, and confirm afterwards that the orders were cancelled.
Two things make this safer than it sounds. order_no is idempotent: re-sending the
same one returns 409 duplicate_reference rather than creating a second order, so a retry
loop in a half-built client cannot flood the system. And a read-only token cannot create
an order at all — do most of your development on one and switch to a writing token only
for the order step.
Changelog and breaking changes
The changelog is at /docs/changelog. It records every change to the API surface, newest first, with the date it reached production.
The versioning commitment lives in full on that page. In short:
- Breaking changes get at least 90 days' notice, announced on the changelog before
they take effect, and ship as a new version path (
/v2/...) rather than by changing/v1/...underneath you. - Additive changes ship any time and are announced, not notified in advance. A new optional field on a response, a new optional request field, a new endpoint, a new error message — none of these require you to do anything. Parse responses tolerantly: ignore fields you do not recognise rather than rejecting the payload.
- Error codes are a fixed set. A new HTTP status on an existing operation counts as
breaking; a new
detailsshape under an existing code does not.
Something here unanswered, or answered wrongly? Tell your account contact — this page is maintained, not archived.