Conventions
How the API represents amounts, identifiers and dates, and the compatibility contract that lets us keep improving v1 without breaking what you have built.
Response envelope
Every response - success or failure, on every endpoint - is an object with data and error. Exactly one is null.
{
"data": { "quoteId": "8f1d0c2a-…", "totalCents": 41700 },
"error": null
}details appears only when there is something structured to add, and its shape depends on the code. It is there to save you a support email; never branch on it.
Amounts
Money is always an integer number of cents, in a field ending Cents, alongside an explicit currency of AUD. There are no decimals anywhere in the API, so there is nothing to round and no float to lose precision.
totalCents is what the account is charged. gstCents is the tax already contained within that total, reported separately for your bookkeeping - it is a component of the total, never an addition to it.
Format for display at the last possible moment, and never parse a formatted amount back into a number.
Identifiers
Treat every identifier as an opaque string. They happen to be UUIDs today; do not parse them, sort by them, or infer anything from their shape or order.
Your own references travel alongside ours. A quote line takes an externalReference - your SKU or line id - which we echo back on the quote and on every order and webhook that follows from it. It is never priced and never interpreted, so it is the right place to carry whatever you need to reconcile against your own system.
Dates
All timestamps are ISO 8601 in UTC. The studio operates on Australian Eastern time, so a promisedReadyDate is a studio-local calendar date rather than an instant - do not shift it into another zone.
Pagination
Lists are cursor-paginated. Pass limit (up to 100) and the nextCursor from the previous page. When nextCursor is null you have reached the end.
let cursor: string | null = null;
do {
const url = new URL(API + "/orders");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
const page = await res.json();
for (const order of page.data) handle(order);
// Stop on nextCursor, never on a short page.
cursor = page.nextCursor;
} while (cursor);Stop on a null cursor rather than on a page shorter than limit - the two are not the same thing.
Forward compatibility
/v1 is meant to last, which it can only do if you know up front which changes to absorb. Otherwise every field we add is somebody's outage, and we end up cutting a /v2 for a reason that was never substantive.
We may do these at any time, without warning
- Add a field to any response object, at any depth.
- Add an error code. The published list is not closed.
- Add a value to any string field that reads like an enum -
productSlug,discountReason,storefrontTier,surfaceType, order status, delivery method ids. - Add an optional request field, or a new endpoint.
- Change any human-readable
message. They are for your logs and your support staff.
We will not do these without a new version
- Remove or rename a field.
- Remove an error code.
- Narrow a type, or change what a field means.
- Make an optional request field required.
One consequence worth knowing: unknown request fields are ignored rather than rejected, so that a client written against a newer build keeps working against an older server. It also means a misspelled field name is silently dropped - misspell shippingMethodId and you get a shipping error listing the valid methods, not a complaint about the typo.