# EziBrand API — guide for coding agents

> Machine-readable spec: https://integrate.ezibrand.com.au/developers/openapi.json
> Everything in one fetch: https://integrate.ezibrand.com.au/developers/llms-full.txt
> Is it us or you: https://integrate.ezibrand.com.au/status.json (JSON) · https://integrate.ezibrand.com.au/status (page)

Order decoration programmatically: garments, embroidery, screenprint and transfers, quoted, submitted
and tracked to despatch.

This is the same API the EziBrand dashboard runs on — not a reduced side-door version. Anything the
dashboard does, your system can do.

## Authenticate

OAuth2 client credentials. `POST https://login.ezibrand.com.au/realms/ezibrand/protocol/openid-connect/token` with `client_id` and
`client_secret` (form-encoded) returns a bearer token; send it as
`Authorization: Bearer <token>` to `https://api.ezibrand.com.au`. Tokens last 10 minutes. Credentials are
issued per wholesale account and see only that account's data.

⚠️ The token endpoint is on the **auth** host, which is not the API host — every call after it goes
to `https://api.ezibrand.com.au`.

Getting them still involves a person: ask your account manager. Self-service issuing is not available yet.

## What you are actually ordering

A job is decoration applied to a garment. So every order answers two questions — **what is being
decorated**, and **what is being done to it** — and the API has a line type for each answer.

There are two ways to supply the garment:

- **We supply it from stock.** `GET /Stock` is the stocked-goods feed: garments held on the shelf,
  each with variants and their own size runs. A `"Stock"` line orders them by size.
- **You send it in.** An `"ExternalGarment"` line is a garment you are shipping to us, counted by
  size. ⚠️ The goods-in record is created by us when the delivery is booked in, not by you — the
  API exposes it read-only, so pre-advising a shipment over the API is not available yet. Reference
  your order number on the job and tell your account manager what is coming.

⚠️ There is a third thing you can order, and it does not work like this at all: a **BOM** — a
made-to-order garment that already carries its decoration. It has its own section below, and if
you are selling finished garments on your own website it is probably the only part of this document
you need.

And two ways to specify the decoration:

- **`"PriceCode"`** — a new one. `GET /PriceCodes/price-codes` gives the codes your account can
  order at your prices; the artwork goes on the line as `externalArtworkUrl`.
- **`"Asset"`** — one we have already made for you. Send the asset tag and it reproduces exactly:
  no artwork, no re-approval, no setup charge.

## Attaching a decoration to a garment

A decoration line and the garment line it goes on are joined by name, not by order in the array.
Give the garment line a `groupHead`, and every decoration going on it the matching `group`:

```json
{
  "items": [
    { "itemType": "ExternalGarment", "groupHead": "navy-polos", "garment": "Navy polo, brand supplied",
      "description": "Arriving under PO-10482, 3 cartons",
      "sizeQuantities": { "S": 10, "M": 20, "L": 15 } },
    { "itemType": "PriceCode", "group": "navy-polos", "code": "<a code from /PriceCodes/price-codes>",
      "quantity": 45, "externalArtworkUrl": "https://<an origin approved on your account>/left-chest.pdf",
      "attributes": { "Position": "Left chest 80mmW", "garment": "Navy polo" } }
  ]
}
```

⚠️ `"Bom"` lines cannot be grouped, and that is deliberate: a BOM already names its own garment and
its own decoration, so there is nothing to attach it to.

## Goods you send in

When you are supplying the garments, the delivery is its own record. `GET /Inwards` lists what you
have sent — order number, supplier, carton count, per-size quantities and where each delivery has got
to — and `GET /Inwards/{id}` opens one up. `GET /Lookups/inwards-statuses` is the status
vocabulary; read it rather than matching strings.

The job tells you the other half. `GET /Jobs/{jobNumber}` carries `stockStatus` —
`Stock Complete`, `Partial Stock` or `No Stock` — which is how you know whether a job is waiting
on a delivery that has not landed yet. Poll that, not a calendar.

## The ordering flow

1. **`GET /PriceCodes/processes`** — the processes your account can order (Embroidery, Screen
   Printing, Digital Transfers …). Start here: availability is per-account, so never hardcode it.
2. **`GET /PriceCodes/price-codes`** — codes for those processes at *your* tier, each carrying the
   `attributes` it needs you to fill in. The `priceCode` returned is what goes in a job line.
3. **`GET /Stock`** — the garments we can supply, if we are supplying them.
4. **`GET /Jobs/delivery-options`** and **`GET /Jobs/earliest-ship-date`** — the delivery methods
   available to you, and the soonest despatch. The method code goes in
   `deliveryAddress.deliveryMethod`.
5. **`POST /Jobs`** — create the job. ⚠️ Send `"validateOnly": true` first: it runs the full
   validation path and returns the errors without creating anything. Send the COMPLETE payload,
   `externalArtworkUrl` included — validation checks the artwork URL without fetching it — then
   send the same bytes again with `validateOnly` false to create the job.
6. **`GET /Jobs/{jobNumber}`** — status, stock status, money, tracking. **`GET /Jobs/active`**
   returns everything open in one call; prefer it over polling jobs individually.

## Artwork goes on the job line

Put a URL to the artwork in `externalArtworkUrl` on each `PriceCode` line. There is no separate
upload step: one request places the order and delivers the art together.

**Put it in the dry run too.** Validation checks the URL and creates nothing, so the payload you
prove is the payload you send. Three rules, each answering by number (measured 2026-09-01):

- **Absolute `https://`.** `http://`, a bare host, a relative or protocol-relative path and any
  other scheme are error `10408`, as is a whitespace-only string.
- **On an origin approved for your account.** Anything else is `10403`; `10409` means no origins
  are approved on the account at all. Both are account configuration — ask us to approve the origin
  you serve artwork from rather than reshaping the URL. ⚠️ Reachability is NOT checked here: a URL
  that resolves and one that does not answer identically, so neither error is about the file.
- **All-or-nothing across the job.** If one `PriceCode` line carries artwork, every `PriceCode`
  line must, or the job is `10404` naming the line that does not. `Asset` lines are repeats and
  are exempt.

⚠️ Omitting the property, `null` and `""` skip all three checks and validate. That is not the
artwork passing — it is the artwork not being looked at, and the URL you then send for real has
never been proved.

The URL must stay reachable until the job is in production — we fetch it, we do not hold a copy of
your link. A signed URL is fine as long as it outlives the job reaching the factory.

## After you have ordered

A job is not frozen the moment you submit it. `GET /Jobs/{jobNumber}` tells you what you may still
do, and you should read that rather than assume:

- **`permissions.canEdit`** — whether **`PATCH /Jobs/{jobNumber}`** will be accepted. You can
  amend the description, your PO number, comments, the requested ship date and the must-ship flag.
  When it is `false`, `permissions.lockedReason` says why.
- **`isCancelable`** — whether **`POST /Jobs/{jobNumber}/cancel`** will be accepted. Once the job
  is in production it will not be, and you get a `409`.

⚠️ They are independent, and they do not change together. A job already in production is commonly
`canEdit: true` and `isCancelable: false` — you can still fix the PO number, but the order is
past the point of being called back. Read both, rather than inferring one from the other or caching
an earlier answer.

## Reordering costs you nothing

An asset is decoration already made — a digitised embroidery file, a set of screens, a separation.
`GET /Assets` lists yours. To run it again, send a job line with `"itemType": "Asset"` and the
asset tag as `code`: no artwork upload, no re-approval, and the result matches the previous run.

## Made-to-order garments (BOMs)

⛔ **A different product, not a fourth line type.** Everything above composes a job out of a garment
and a decoration. A BOM is already both: a finished, made-to-order garment with its decoration
specified, ordered as one line. If you sell garments on your own website, this is the whole
integration and you can skip the ordering machinery entirely — no price codes, no attributes, no
artwork URL, no grouping.

```json
{
  "orderNumber": "WEB-2291",
  "dateDue": "2026-09-14",
  "items": [
    { "itemType": "Bom", "code": "BOM10112-M", "quantity": 2 },
    { "itemType": "Bom", "code": "BOM10112-L", "quantity": 1, "customName": "A. Patel" }
  ],
  "deliveryAddress": {
    "contactName": "Sam Patel",
    "organisation": "Riverside Print Co",
    "streetAddress": "12 Tannery Road",
    "city": "Auckland",
    "postalCode": "1010",
    "country": "New Zealand",
    "countryCodeISO2": "NZ",
    "deliveryMethod": "overnight",
    "emailAddress": "sam@example.com"
  }
}
```

A code, a size and a quantity. That is the whole line.

**How to wire it to your storefront**

1. **`GET /Boms`** on a schedule — your catalogue. ⚠️ It answers with EVERY BOM in one unpaged
   response (thousands of records, several megabytes), so cache it; never call it per order or per
   page render.
2. **Map it to your own product data with the SKU fields.** A BOM carries `customerSku`, and each
   variant carries `customerVariantSku` — *your* codes, stored against our records. Set them and an
   order for `RRC-POLO-NAVY-M` on your site becomes a job line with no lookup table of your own to
   maintain.
3. **The variants are already in that response** — `bomVariants`, one per size, each with its
   own `bomVariantCode`. ⛔ Do NOT loop `GET /Boms/{bomCode}` to collect them: you would make
   thousands of requests for data you have already downloaded. That endpoint is for refreshing a
   single BOM. ⛔ Order by the `bomVariantCode` you read back. It is *usually*
   `{bomCode}-{size}` and 197 EW variants are not, so building it by concatenation works right up
   until it silently orders a size that does not exist. `size: "Qty"` means one-size-fits-all.
4. **`GET /Jobs/delivery-options`** — the delivery methods THIS account may use. ⚠️ Do not
   copy the `deliveryMethod` from the example above: carriers differ by account, and a method
   your account does not have makes the address unroutable. Read it once and cache it with the
   catalogue.
5. **`POST /Jobs`** with one `"Bom"` line per variant sold. `customName` adds a
   per-item personalisation — a back name from an "add a name" option at your checkout — and
   produces a per-name line rather than one aggregated one. ⛔ **ALWAYS SEND `size` ON A BOM
   LINE.** Where the code does not already carry one, omitting `size` — or sending `null` —
   answers an opaque **500**, while `size: ""` reaches the real validator and returns a
   structured `1825 "Size is required"`. So the crash is the *absent* case, not the invalid one:
   send the `size` from `bomVariants` and neither happens. ⚠️ This warning named `dateDue`
   until 2026-08-29, which was a misattribution — measured 2×2 that day, `dateDue` present or
   absent makes no difference to this 500, and omitting it is fine on every line type.
6. **`GET /Jobs/{jobNumber}`** to track it out, the same as any other job.

⛔ **A BOM line cannot be grouped.** `groupHead` and `group` attach a decoration to a garment,
and a BOM has no separate decoration to attach — it is the finished article. Sending either is a
schema error, not a no-op.

**Pricing is deliberately not on the BOM.** A BOM total is the sum of its components, and each
component publishes its own quantity breaks: `GET /Assets/{assetTag}` carries `priceBands`,
`setup` and `reset` for a decoration, `GET /Stock/{stockCode}` carries `priceBands` for the
garment. One BOM-level figure could not express a quantity break, which is the thing that moves on
a real order. ⚠️ The component list is not on the read yet, so you cannot assemble that total
today — ask your account manager for a price list meanwhile. Same for the BOM image. Both are in
hand.

⛔ **A BOM with no variants cannot be ordered, and nothing says so.** Measured on a real catalogue:
120 of 5,724 come back with an empty `bomVariants`. Skip them when you sync, or you will list a
product with no size to order.

⚠️ **`title` is the name, not `description`.** On that catalogue `title` was populated on all
but one BOM and `description` on 54 — a listing built from `description` is blank 99% of the
time.

⚠️ **The SKU mapping has to be filled in.** 141 of those BOMs carried a `customerSku` and 1,148
variants a `customerVariantSku`. Both are yours to define — send us the mapping for the products
you sell rather than assuming it is already there.

## Conventions that will bite you otherwise

- **`items[].itemType` selects the line type**, and it decides what `code` means. `"PriceCode"`
  (a full price code), `"Asset"` (an asset tag), `"Stock"` (a stock code), `"Bom"` (a BOM code)
  and `"ExternalGarment"` (no code — you name the garment). These are published as an enum on
  `JobLineType` and as the `itemType` discriminator on the line union, so a generated client
  already knows them. ⚠️ `"Bom"` belongs to the made-to-order path above and
  mixes badly with the rest: it carries its own decoration and refuses the grouping fields.
- **Never build a size variant code by hand.** Take `bomVariantCode` from
  `GET /Boms/{bomCode}`. It is *usually* `{bomCode}-{size}` and 197 EW variants are not, so
  concatenating works right up until it silently orders the wrong size.
- **Size runs come from the data, not from your own ordering.** A size set is a named, ordered list
  from `GET /Lookups/size-sets`; sorting the sizes yourself puts `10` before `2` and `XL`
  before `XS`.
- **A 500 from `POST /Jobs` usually means an invalid payload, not an outage.** Empty `items`, an
  unroutable delivery address, a nonexistent asset code or a bad `sizeQuantities` key all surface
  as an opaque 500. Fix the payload before retrying; `validateOnly` with a well-formed payload
  returns real validation messages. If you need to rule out the other possibility, https://integrate.ezibrand.com.au/status.json reports what our own monitoring sees — ⛔ read its `monitoring.usableForDiagnosis` FIRST: when that is false every component reads `unknown` because we could not reach our own monitoring, which is neither reassurance nor alarm, and your own retry is the better signal.
  Two triggers are isolated and worth checking first, because each has a payload edit that turns
  the crash into a readable error: a `Bom` line whose `size` is absent or `null` (send it —
  `""` alone reaches the real `1825`), and a delivery address with no `emailAddress` (send
  one; every other missing address field returns a numbered error).
- **Read enums, don't hardcode them.** Job statuses come from `GET /Lookups/job-statuses`,
  processes from `GET /Lookups/process-codes`, inwards statuses from
  `GET /Lookups/inwards-statuses`, countries and states from `GET /Lookups/countries`.
- **Money is in the account's currency.** No conversion is applied anywhere.
- **`countryCodeISO2` is two characters** — `"NZ"`, not `"NZL"`. A three-letter code is rejected.
- **4xx bodies carry the reason.** Every operation documents its failure codes; read the body rather
  than retrying blind.

## Working rules

These are the things that are not visible from the schema and that cost time when discovered the
hard way.

1. **Build against `validateOnly`.** `POST /Jobs` with `"validateOnly": true` runs the entire
   authentication and validation path and creates nothing, answering
   `"Validated with NO Errors. Job NOT created."` when the payload is good. Use it for every
   iteration until the payload is right.
   ⛔ Validate the payload you are ACTUALLY GOING TO SEND, `externalArtworkUrl` included: the
   dry run checks each artwork URL's shape and origin and neither fetches nor stores it, so the
   only thing that changes when you create the job is the flag. Blanking the artwork to get a
   clean dry run defeats the point — an omitted, `null` or `""` URL is not checked at all.
2. **A 500 from `POST /Jobs` usually means your payload, not an outage.** Measured triggers: an
   empty `items` array, a delivery address that cannot be routed or carrying no `emailAddress`,
   an unknown asset code, a `sizeQuantities` key that is not in the variant's size run, and a
   `Bom` line whose `size` is absent or `null`. Retrying unchanged will not help. To rule out the other possibility,
   https://integrate.ezibrand.com.au/status.json reports what our own monitoring sees for this region — including failures
   that start with the systems we depend on. ⛔ Read its `monitoring.usableForDiagnosis` first:
   when that is false, every component reads `unknown` because we could not reach our own
   monitoring, which is neither reassurance nor alarm.
   ⚠️ A missing `dateDue` was listed here until 2026-08-29 and is NOT a trigger — measured 2×2
   that day against the BOM 500 it was blamed for, its presence or absence made no difference;
   `size` was the cause. Omitting it validates on every line type.
3. **Most rejections are repairable, and say so by number.** A 400 carries
   `validationMessages[].errorCode`, and these are the ones you will meet: `1700` unknown asset ·
   `1800` stock code not found (send it exactly as `GET /Stock` returned it, leading whitespace
   included) · `1820` a size not in that variant's run, which lists the valid sizes ·
   `1823` unknown BOM code · `1825` BOM `size` required · `1828` a `group` with no
   matching `groupHead`, naming the orphan · `1830` duplicate `customerReference` — remember
   blank counts as a value, so two lines that both omit it collide · `1900`/`1901` an
   `ExternalGarment` missing its `garment` or `description` · `10302` a delivery method
   this account cannot use, which lists the valid ones · `10303` a `dateDue` in the past ·
   `10403` an `externalArtworkUrl` whose ORIGIN is not approved for this account ·
   `10404` one `PriceCode` line carries artwork and another does not · `10408` an
   `externalArtworkUrl` that is not an absolute http/https URL · `10409` no approved artwork
   origins are configured on this account at all.
   Fix the named thing and re-validate; none of these is worth a retry. ⚠️ `10403` and
   `10409` are the two you cannot fix in your payload — they are account configuration, so ask
   us to approve the origin you serve artwork from.
4. **`items[].itemType` decides what `code` means.** `"PriceCode"` takes a full price-code
   string, `"Asset"` takes an asset tag, `"Stock"` takes a stock code, `"Bom"` takes a BOM
   variant code, and `"ExternalGarment"` takes no code at all — you name the garment and count it
   per size in `sizeQuantities`. These are published as an enum on `JobLineType` and as the
   `itemType` discriminator on the line union, so a generated client already knows them.
   ⛔ Attach a decoration to the garment it goes on with `groupHead` on the garment line and a
   matching `group` on each decoration line. `"Bom"` lines cannot be grouped: a BOM already
   carries its own decoration and does not inherit the grouping fields.
5. **Never hardcode an enum.** Job statuses come from `GET /Lookups/job-statuses`, processes from
   `GET /Lookups/process-codes`, countries and states from `GET /Lookups/countries`. New values
   are added over time and must not break your integration.
6. **A price code's embedded size is a label, not a measurement.** Do not parse
   `DG_DYESUB_RDT:Dye Sub-150cm x 75cm` for geometry; read `sizeWidth`, `sizeHeight` and
   `sizeUnit`. ⛔ And where a code carries `DG-X`/`DG-Y`, those are in the code's OWN unit,
   which is NOT millimetres on EW's cad-cut codes — `sizeUnit` is null on exactly those, so ask
   rather than assume. Getting it wrong misquotes by 10× per axis.
7. **`countryCodeISO2` is two characters.** `"US"`, never `"USA"`.
8. **Prices are per-account.** Everything you read is at that account's tier. Never cache pricing
   across accounts or show it to another customer.
9. **Reordering is the cheap path.** An existing asset reordered by tag reproduces the previous run
   with no artwork to supply and no colour re-approval.
10. **Artwork goes on the job line as `externalArtworkUrl`.** There is no separate upload step. It
   must be an ABSOLUTE `https://` URL on an origin approved for your account, and if any
   `PriceCode` line carries artwork then every one of them must. The URL must stay reachable
   until the job is in production — we fetch it rather than holding your link — but reachability is
   NOT what validation checks, so a URL that 404s still validates and a perfectly good one on an
   unapproved origin does not.
11. **Check the job before amending or cancelling it.** `GET /Jobs/{jobNumber}` returns
    `permissions.canEdit` (whether `PATCH /Jobs/{jobNumber}` will work, with
    `permissions.lockedReason` when it will not) and `isCancelable` (whether
    `POST /Jobs/{jobNumber}/cancel` will work). ⚠️ They are independent: a job in production is
    commonly still editable but no longer cancellable. Read both rather than inferring one from the
    other.
12. **Region is a property of the credentials.** You cannot discover it by calling the API — you need
    the right regional host to get a token at all. It is stated when the credentials are issued.

## Concepts

Things no single endpoint owns, and the ones most often got wrong.

### Attributes: the form the API hands you

Every price code carries its own input fields. Read them, render them, send them back — never hardcode them.

Decoration is not just a size and a quantity. The factory needs to know which garment it is going on, where on the garment, which colours, what to call it on the job sheet. Those questions differ per product, and they change. So the API tells you what to ask.

Every price code returned by GET /PriceCodes/price-codes carries an `attributes` array. Each entry is a field definition: what to call it, what type of input, whether it is required, what the allowed values are. You build your form from that array, collect the answers as a flat object, and send it back on the job line.

```json
// What a price code tells you to ask (from GET /PriceCodes/price-codes)
{
  "priceCode": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
  "minimumQuantity": 1,
  "attributes": [
    {
      "name": "garment",
      "label": "Garment",
      "description": "Enter garment code colour description here",
      "type": "text",
      "maxLength": 200,
      "required": false,
      "isMetaDataAttribute": false,
      "enumerableValues": null
    },
    {
      "name": "Position",
      "label": "Position",
      "description": "ie Front 50mmD",
      "type": "text",
      "maxLength": 100,
      "required": false,
      "enumerableValues": null
    },
    {
      "name": "Colours",
      "label": "Colours",
      "description": "Enter specific PMS or CMYK",
      "type": "text",
      "maxLength": 100,
      "required": false,
      "enumerableValues": null
    },
    {
      "name": "DG-X",
      "label": "Width",
      "type": "decimal",
      "required": true,
      "isMetaDataAttribute": true
    },
    {
      "name": "DG-Y",
      "label": "Height",
      "type": "decimal",
      "required": true,
      "isMetaDataAttribute": true
    }
  ]
}
```

Render `type: "select"` as a dropdown of `enumerableValues`, `type: "text"` as a text input bounded by `maxLength`, `type: "decimal"` as a number. Use `label` for the human and `description` as the hint — several EW attributes carry a worked example in it, such as "ie Front 50mmD". Then send what the user chose, keyed by `name`:

```json
// What you send back on the job line
{
  "itemType": "PriceCode",
  "group": "navy-polos",
  "code": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
  "quantity": 45,
  "externalArtworkUrl": "https://files.example.com/riverside/crest.pdf",
  "attributes": {
    "description": "Club crest, left chest",
    "garment": "Navy polo",
    "Position": "Left chest 80mmW",
    "Colours": "PMS 288C, PMS 186C",
    "DG-X": 8,
    "DG-Y": 6.5
  }
}
```

> ⛔ Never hardcode the key names, and never assume the set. Attributes are per price code and they change: an embroidery code and a cad-cut code do not ask the same questions, and a code that gains a field gains it for you too. Always key off the `name` the API gave you, exactly — the keys are case-sensitive, and `Colours` is not `colours`.

> ⚠️ `attributes` is a hierarchy, not one flat shape. The base carries `name`, `label`, `type`, `required`, `description`, `enumerableValues`, `isMetaDataAttribute` and `assetAttributeName`; text attributes add `maxLength` and `minLength`. The specification does not discriminate the subtypes, so branch on `type` rather than assuming every field is present.

Truncate each value to its own `maxLength` before submitting — an over-long value is rejected at the far end, not helpfully.

Where the attributes include `DG-X` and `DG-Y` with `isMetaDataAttribute: true`, this is a custom-dimension product — a cad-cut film or flock cut to size. Those two are not questions for the customer, they are the width and the height, they are `required`, and they drive the price. Pass them to GET /PriceCodes/{priceCode}?dgX=…&dgY=… to get the price for that size.

> ⛔ Build the form from the LIST, never from the single-code read. Measured on 2026-08-28: `GET /PriceCodes/price-codes` returns `DG_DIGICAD_FLEX220` with `hasMetaDataAttributes: true` and both `DG-X` and `DG-Y`, while `GET /PriceCodes/{priceCode}` returns the same code with `hasMetaDataAttributes: null` and NO metadata attributes at all. An integration that builds its inputs from the single-code endpoint therefore never discovers the two dimensions the code cannot be priced or ordered without — and nothing in the response says they are missing. Read attributes from the list, and use the single-code endpoint for what it is good at: pricing a size you already know.

> ⛔ `DG-X` and `DG-Y` are NOT millimetres, and getting the unit wrong misquotes by 10× per axis — 100× on area. The unit belongs to the price code, not to the region: read it from `sizeUnit` on the price code, and note that `sizeUnit` is **null on exactly the calculated codes that need it**, because Codewolf populates it only where it can parse a size label. On those, ask us for the unit rather than assuming — the example above sends 8 × 6.5 for a decoration `Position` describes as 80mm wide. ⚠️ Do NOT take the unit from the `DG-X` attribute description: it reads "In centimetres" in every region including ones where that is wrong, so it is not evidence.

### What is being decorated, and what is being done to it

A job line is either a garment or a decoration. Five line types, and one rule that joins them.

Every order answers two questions, so `POST /Jobs` has a line type for each. `items[].itemType` is published as an ENUM and as the discriminator for the line-shape union, so a generated client already knows the values: `Asset`, `PriceCode`, `Stock`, `Bom`, `ExternalGarment`. It also decides what `code` means on that line — the list below says what each one takes.

- `"Bom"` — a made-to-order garment, decoration included, ordered by its variant code. ⚠️ It belongs to a different flow, not this one — see “Made-to-order garments”.
- `"Stock"` — a stocked item we hold, by `stockCode`, with a `variantCode` and `sizeQuantities`.
- `"ExternalGarment"` — a garment you are sending in. No code: you name the garment and count it per size in `sizeQuantities`.
- `"PriceCode"` — a decoration we have not made before, by full price code, with the artwork as `externalArtworkUrl`.
- `"Asset"` — a decoration we have made before, by asset tag. No artwork, no re-approval, no setup charge.

A decoration line and the garment line it goes on are joined by NAME, not by their order in the array. Put a `groupHead` on the garment and the matching `group` on every decoration going onto it. The name is yours and it only has to be unique within the job.

```json
// Two decorations on one set of garments
{
  "items": [
    {
      "itemType": "ExternalGarment",
      "groupHead": "navy-polos",
      "garment": "Navy polo — customer supplied",
      "description": "Arriving under PO-10482, 3 cartons",
      "sizeQuantities": { "S": 10, "M": 20, "L": 15 }
    },
    {
      "itemType": "PriceCode",
      "group": "navy-polos",
      "code": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
      "quantity": 45,
      "externalArtworkUrl": "https://files.example.com/riverside/crest.pdf",
      "attributes": { "Position": "Left chest 80mmW" }
    },
    {
      "itemType": "Asset",
      "group": "navy-polos",
      "code": "EW49123",
      "quantity": 45
    }
  ]
}
```

> ⛔ `"Bom"` lines cannot be grouped, and that is the tell that they are not really part of this model: `JobLineBom` is the one line type that does not inherit the grouping fields. A BOM already names its own garment and its own decoration, so there is nothing to attach it to, and sending `group` on one is a schema error rather than a no-op. If BOMs are what you are ordering, read “Made-to-order garments” instead of this — none of the composition below applies.

> ⚠️ Quantities are counted differently by line type, and they must agree. A garment line counts per size in `sizeQuantities`; a decoration line carries a flat `quantity`. If 45 garments are going in and the decoration says 40, that is 40 decorated garments and 5 plain ones — which is a real thing to order, so nothing will stop you.

### Asking when it can ship

One call, four fields, and one thing it does not currently do — measured rather than assumed.

GET /Jobs/earliest-ship-date is the only honest source for a despatch date: it accounts for factory workload and cut-offs, so a fixed lead time in your own code will be wrong. It answers with all four of these.

```json
// The response
{
  "shippingDateUtc":   "2026-09-04T02:00:00+00:00",
  "shippingDateLocal": "2026-09-04T14:00:00+12:00",
  "timezone":          "Pacific/Auckland",
  "dateExclusions":    []
}
```

> ⛔ Take the date from `shippingDateLocal`, not from the UTC instant. `dateDue` is a calendar date in the factory’s terms, and the two disagree either side of midnight — the same moment is the 4th locally and the 3rd in UTC for part of every day. `timezone` is an IANA name so you can do the conversion properly rather than guessing an offset.

> ⚠️ `processCodes` currently makes no difference here. Measured on 2026-08-29: `EW`, `MG`, `EW,MG`, no parameter at all, and a process code that does not exist ALL returned the same date. Send the codes you are ordering — it is the documented input and the behaviour may bind later — but do not build per-process lead times on it, and do not treat a future difference as a bug.

What the API does not tell you, and we will not invent: whether a repeat `Asset` decoration contributes a process code, and whether `Stock` or `Bom` lines enter the calculation at all. The endpoint takes only process codes, so there is no input by which a stock or BOM line could affect it. Treat the answer as a floor for the whole job.

`dateDue` is a request, not a promise. Setting `mustDate: true` alongside it commits the factory to that date rather than treating it as a preference — so only set it when the date genuinely cannot move. ⚠️ We have not measured what the API does when `mustDate: true` carries a date earlier than the earliest ship date; check the response rather than assuming it is accepted.

### One job, start to finish

A garment we stock, a new decoration and a repeat on the same garments — validated first, then created.

This is the shape a real order takes: one garment line, two decorations attached to it, several sizes, and an address. ⚠️ Every identifier is a PLACEHOLDER — read your own from the catalogue. The stock code, the variant, the price code, its attributes, the asset tag and the delivery method are all per-account.

It goes out TWICE: once to validate, once to create. The two payloads are IDENTICAL apart from `validateOnly` — that is the point of a dry run, and it is why the artwork URL belongs in both.

```json
// Step 1 — validate it
{
  "validateOnly": true,
  "orderNumber": "WEB-2291",
  "dateDue": "2026-09-14",
  "description": "Riverside Rugby — club polos",
  "items": [
    {
      "itemType": "Stock",
      "groupHead": "navy-polos",
      "code": "<stockCode from GET /Stock>",
      "variantCode": "<variantCode from that item's stockVariants>",
      "sizeQuantities": { "S": 10, "M": 20, "L": 15 }
    },
    {
      "itemType": "PriceCode",
      "group": "navy-polos",
      "code": "<priceCode from GET /PriceCodes/price-codes>",
      "quantity": 45,
      "customerReference": "WEB-2291-1",
      "attributes": { "description": "Club crest", "Position": "Left chest" },
      "externalArtworkUrl": "https://<an origin approved on your account>/riverside/crest.pdf"
    },
    {
      "itemType": "Asset",
      "group": "navy-polos",
      "code": "<assetTag from GET /Assets>",
      "quantity": 45,
      "customerReference": "WEB-2291-2"
    }
  ],
  "deliveryAddress": {
    "contactName": "Sam Patel",
    "streetAddress": "12 Tannery Road",
    "city": "Auckland",
    "postalCode": "1010",
    "countryCodeISO2": "NZ",
    "deliveryMethod": "<code from GET /Jobs/delivery-options>",
    "emailAddress": "sam@example.com"
  }
}
```

A good answer is the literal string "Validated with NO Errors. Job NOT created." Anything else is a `validationMessages` array naming the line index and the field.

```json
// Step 2 — create it (one character different)
{
  "validateOnly": false,
  "orderNumber": "WEB-2291",
  "…": "every other byte exactly as you validated it"
}
```

ONE thing changes: `validateOnly` becomes false, or is dropped — it defaults to false. Everything else, the artwork URL included, is sent exactly as validated. The response carries a `jobNumber` and, per decoration line, the asset tag your artwork was filed under.

> ⛔ VALIDATE THE PAYLOAD YOU ARE GOING TO SEND. Measured 2026-09-01: an `externalArtworkUrl` that is omitted, `null` or `""` is not checked at all, and the request validates — so blanking the artwork to get a clean dry run proves nothing about the URL you then create the job with. Sent for real, that same URL can still be rejected for its shape (`10408`) or its origin (`10403`), and the difference between the payload you proved and the payload you sent is the one field you did not prove.

- The garment line carries `groupHead`; both decorations carry the matching `group`. That is what puts them on those garments rather than beside them.
- Sizes live on the garment line as `sizeQuantities`; the decorations carry a flat `quantity` that should equal the run.
- ⛔ An `ExternalGarment` line REQUIRES a non-empty `description` (error 1901) and a `garment` (error 1900) — the schema marks neither. Verified: omitted, empty and grouped-without-one all fail.
- ⛔ Send a `stockCode` exactly as `GET /Stock` returned it. Some carry leading whitespace, and trimming it fails with error 1800 — "no stock item found".
- ⛔ **`externalArtworkUrl` must be an ABSOLUTE `https://` URL on an origin your account has approved.** Measured 2026-09-01: a bare host, a relative or protocol-relative path, `http://` or any other scheme is error `10408`; a well-formed URL on an unapproved origin is `10403`, and `10409` means the account has no approved origins at all. Reachability is not checked during validation — a URL that resolves and one that does not answer identically — so neither of those is about the file. Approving an origin is account configuration; ask us rather than reshaping the URL.
- ⛔ **Artwork is all-or-nothing across the job.** If ONE `PriceCode` line carries `externalArtworkUrl`, every `PriceCode` line must, or the job fails with error `10404` naming the line that does not. `Asset` lines are repeats and never carry artwork, so they are exempt.
- ⛔ **GIVE EVERY LINE ITS OWN `customerReference`.** It is not a required field — one line may leave it out and validate — but it must be UNIQUE ACROSS THE JOB, and *blank counts as a value*. Two lines that both omit it collide exactly like two carrying the same string, and answer error `1830`. Measured 2026-08-29: absent and `null` are the SAME value as each other, `""` is a THIRD distinct value, so at most one line may omit it and at most one may send `""`. ⚠️ Do not parse the number in that message — it reported "1 duplicated values" for three colliding lines and "2" for two, and it names neither the offending value nor the line index. Sending a distinct reference per line sidesteps all of it, and it is how you match a returned asset tag to the line you sent.

### Goods you send in

When you supply the garments, the delivery is its own record — and the job tells you whether it has landed.

An `ExternalGarment` line says what a job needs. An inwards record says what actually turned up. They are separate on purpose: garments are shipped by you, or by your supplier directly, and they arrive on their own schedule.

GET /Inwards lists your deliveries, newest first and paged. Each row carries the order number you shipped under, the supplier, the carton count, the total quantity and — the field that actually matters — `isAllocated` with its `jobNumber` once the delivery has been matched to a job. GET /Inwards/{id} opens one up, item by item, counted per size.

```js
// Deliveries for one job, and everything not yet allocated
// Every delivery recorded against a job
const forJob = await api('/Inwards?jobNumber=612226');

// Everything not yet matched to a job.
// ⚠️ Allocation, not arrival — the spec defines this as job=0, so a delivery still
// in transit is in here too. Read status or dateIn to know whether it has landed.
const unallocated = await api('/Inwards?allocated=false&pageSize=50');
```

> ⚠️ `status` looks like the field to watch and is not. In practice nearly every record reads `Arrived` — the vocabulary is real (GET /Lookups/inwards-statuses) but it has almost no variance. `isAllocated` is where the movement is.

The job tells you the other half. GET /Jobs/{jobNumber} carries `stockStatus` — `Stock Complete`, `Partial Stock` or `No Stock` — which answers "is this job waiting on garments?" across all of its deliveries at once. Poll that rather than reconciling delivery lines against job lines yourself.

### Sizes are an ordered list, not a string you can sort

Size sets come from the API with their order. Sorting them yourself puts 10 before 2 and XL before XS.

Anything counted per size — a stock variant, a BOM, an inwards line — belongs to a named size set. GET /Lookups/size-sets returns them: an id, a name, and the sizes as an ORDERED array. There are 300 of them, because a size run is per product range, not per company.

```json
// A size set
{ "id": 139, "name": "00-1", "sizes": ["00", "0", "1"] }
```

> ⛔ Take the order from `sizes`, never from your own sort. Alphabetically `10` comes before `2` and `XL` before `XS`; numerically the alpha sizes do not sort at all. A size run rendered out of order is a mis-picked order, and it looks like a display bug right up until the wrong box ships.

> ⚠️ A line returns only its NON-ZERO sizes. An inwards item with nothing in S simply has no S entry — so build the row from the size set and fill from the line, rather than reading the line and assuming it is the whole run.

The size string `"Qty"` is not a size. It means one-size-fits-all: a single quantity, no run. Stock equipment and one-size goods use it, and so do BOM variants that have no sizes.

### Keeping a BOM catalogue in step

How to sync the made-to-order catalogue, map it to your own SKUs, and skip the rows that cannot be ordered.

Everything else in this API composes a job out of two things: a garment, and a decoration to put on it. A BOM is already both. It exists so that someone selling made-to-order garments on their own website can send an order through without knowing anything about price codes, stock codes, artwork or grouping.

Ordering one is a code, a size and a quantity — no `attributes`, no `externalArtworkUrl`, no `groupHead` — and the worked payload is in “Made-to-order garments” above. What follows is the part that is not obvious from a single order: keeping the catalogue itself in step.

- GET /Boms — your catalogue, on a schedule. ⚠️ Every BOM in one unpaged response, thousands of records and several megabytes: cache it, never call it per order or per page render.
- GET /Boms/{bomCode} — the variants, one per size. ⛔ Order by the `bomVariantCode` you read back, never by joining the code and the size: 197 EW variants do not follow `{bomCode}-{size}`. A size of `Qty` means one-size-fits-all.
- POST /Jobs — one `"Bom"` line per variant sold.
- GET /Jobs/{jobNumber} — track it out, the same as any other job.

The field that makes this simple is the SKU mapping. A BOM carries `customerSku`, and every variant carries `customerVariantSku` — your own codes, held against our records. Set them and an order for your SKU becomes a job line directly, with no translation table of your own to keep in step.

```json
// Your SKU is already on the variant
{
  "bomCode": "BOM10112",
  "title": "Riverside Rugby — club polo, navy",
  "range": "Riverside Rugby",
  "customerSku": "RRC-POLO-NAVY",
  "bomVariants": [
    { "bomVariantCode": "BOM10112-M",   "size": "M",  "customerVariantSku": "RRC-POLO-NAVY-M" },
    { "bomVariantCode": "BOM10112-72R", "size": "72", "customerVariantSku": null }
  ]
}
```

On pricing: this API does not return a BOM price, and that is deliberate rather than missing. A BOM total is only ever the sum of its components, and each component already publishes its own quantity breaks — `GET /Assets/{assetTag}` gives `priceBands`, `setup` and `reset` for a decoration, `GET /Stock/{stockCode}` gives `priceBands` for the garment. A single BOM-level figure could not express a quantity break, which is exactly what moves on a real order.

> ⚠️ Today the component list is not on the read, so you cannot yet assemble that total yourself — ask your account manager for a price list in the meantime. The same applies to the BOM image. Both are in hand.

> ⛔ A BOM with no variants cannot be ordered, and nothing in the response says so — measured on a real catalogue, 120 of 5,724 BOMs come back with an empty `bomVariants`. Skip them when you sync, or you will list a product on your site that has no size to order. You also cannot tell one that was never finished from one that has been retired.

> ⚠️ Use `title` as the name, not `description`. On the same catalogue `title` was populated on all but one of 5,724 BOMs, and `description` on 54 — so a listing built from `description` is blank 99% of the time.

> ⚠️ The SKU mapping only works if it is filled in: 141 of those BOMs carried a `customerSku` and 1,148 variants a `customerVariantSku`. Both are yours to define — send us the mapping for the products you sell rather than assuming it is already there.

> ⛔ Do not try to attach a decoration to a BOM. `groupHead`/`group` exist to join a decoration to a garment, and a BOM has no separate decoration to join — it is the finished article. Ordering a decorated garment a different way is what the rest of this documentation is about.

### Pull the catalogue once, not per order

Availability and prices are per-account. Fetch them on a schedule, store them, and read your own copy at checkout.

GET /PriceCodes/processes tells you what this account may order; GET /PriceCodes/price-codes returns the codes and their prices at that account’s tier. Neither is a public list — two customers calling the same endpoint get different answers. The same is true of GET /Boms and GET /Stock: your BOM catalogue is yours.

Fetch both on a schedule and store them. Read your stored copy when someone is building an order. That is faster at checkout, it survives a blip on our side, and it is where the attribute definitions come from — you do not need a live call per order to know what to ask.

```js
// Setup, then per-order
// Setup — on a schedule, e.g. nightly.
// ⛔ GET /Boms is thousands of records and several megabytes, unpaged. Never per order.
const processes  = await api('/PriceCodes/processes');
const priceCodes = await api('/PriceCodes/price-codes');
const boms       = await api('/Boms');
await store.replaceCatalogue({ processes, priceCodes, boms });

// Per order — from your own store, no network call
const code = await store.findPriceCode(chosenCode);
renderAttributeForm(code.attributes);
```

> ⛔ Read the minimum off `minimumQuantity` and the first band’s `from`, rather than assuming 1. Most EW codes do start at 1, and enough of them do not — several dye-sublimation codes start at 20 — that a hardcoded floor lets a customer build a basket the API then refuses at checkout.

> ⚠️ Refresh it. Prices and availability change, and a stale catalogue quotes a number your customer will not be billed. Treat your copy as a cache with an expiry, not as a fixture you ship once.

The one thing not to cache is the ship date. GET /Jobs/earliest-ship-date accounts for factory workload and cut-off times, so it is a live answer by design.

### Paging, sorting and filtering

The list endpoints share one set of query parameters. Learn them once.

Most list endpoints take the same shape, so you can write the plumbing once and reuse it for assets, jobs, stock and price codes.

```json
// The shared parameters
?page=1&pageSize=25
?sortColumn=DateDue&sortDirection=Descending
?filter=riverside          # free-text search
?includeProcesses=WE,BL    # only these product families
?excludeProcesses=NA,NU    # everything but these
```

Paged responses carry `totalCount`, `totalPages`, `hasNextPage` and `hasPreviousPage` alongside the rows, so you can drive a pager without counting.

> ⚠️ Not every parameter applies to every endpoint, and a few use `searchText` or `sortBy` instead. The specification lists the exact query parameters per operation — treat this as the pattern and the reference as the authority.

### Delivery addresses

Seven fields are required, one of them fails without saying so, and the delivery method is a code you must read from the API.

Every field name below was verified against the live API on 2026-08-29 with `validateOnly` dry runs — one field removed at a time — so this is the shape the server accepts, not a shape derived from the schema.

```json
// A delivery address that validates
{
  "contactName": "Sam Patel",
  "streetAddress": "12 Tannery Road",
  "city": "Auckland",
  "postalCode": "1010",
  "countryCodeISO2": "NZ",
  "deliveryMethod": "overnight",
  "emailAddress": "sam@example.com",

  "organisation": "Riverside Print Co",
  "phone": "+64 9 555 0100",
  "country": "New Zealand",
  "address2": "Unit 4",
  "suburb": "Grey Lynn",
  "state": null,
  "shippingInstructions": "Leave at the loading dock",
  "isSaturdayDelivery": false
}
```

The first seven are required; the rest are optional. Remove a required one and the API names it — `contactName` (1500), `streetAddress` (1510), `city` (1525), `postalCode` (1530), `countryCodeISO2` (1540), `deliveryMethod` (10302).

> ⛔ EXCEPT `emailAddress`, WHICH FAILS SILENTLY. Omit it and the request answers an opaque 500 with no validation message at all — the one required field whose absence you cannot diagnose from the response. If a job 500s and everything else looks right, check that you sent an email address.

> ⛔ THESE NAMES, EXACTLY. `DeliveryAddress` declares `additionalProperties: false`, so a plausible-looking synonym is not ignored — it fails. `companyName`, `addressLine1`, `postCode`, `countryCode` and `contactPhone` are NOT accepted: the correct names are `organisation`, `streetAddress`, `postalCode`, `countryCodeISO2` and `phone`. ⚠️ Note `countryCode` IS correct on the ACCOUNT address returned by `GET /account` — two different shapes, and the one that ships a job is this one.

`country` is optional when `countryCodeISO2` is present. `countryCodeISO2` is two characters — `"NZ"`, not `"NZL"`.

For regions with states or provinces, GET /Lookups/countries/{countryCode}/states gives the accepted values. Send the value the lookup gives you — a full state name where the lookup returns a full state name, not an abbreviation you shortened yourself.

### Which value is the delivery method

GET /Jobs/delivery-options returns three fields that look usable. Exactly one of them is.

Each option carries a `code`, a `value` and a `label`. ⛔ Send the `code`. The `label` is for display and is REJECTED on submission — verified: `"2day"` (the code) validates, `"2 DAY"` (its label) fails with error 10302, and so does `"BULK FREIGHT"` where the code is `"Bulk"`.

```json
// What an option looks like, and what to send
// GET /Jobs/delivery-options
[
  { "code": "overnight", "value": "overnight", "label": "OVERNIGHT", "isCollection": false, "rank": 2 },
  { "code": "2day",      "value": "2day",      "label": "2 DAY",     "isCollection": false, "rank": 3 }
]

// POST /Jobs
"deliveryAddress": { "deliveryMethod": "2day" }   // the code — NOT "2 DAY"
```

Matching is case-insensitive: `"OVERNIGHT"` is accepted for the code `overnight`. That is why a label can appear to work — it only breaks where the label differs from the code by more than case, as `2 DAY` and `BULK FREIGHT` do. Do not rely on it. `value` duplicated `code` on every option measured; prefer `code`, which is what our own ordering submits.

> ⚠️ Read the options for the account you are ordering for. Carriers are per-region and per-account, and a code that works for one customer may not exist for another. ⛔ An invalid method is one of the few mistakes the API reports clearly — it answers `Delivery method "X" is invalid, must be one of "…"` and lists the set it will accept, which is broader than any one account is offered. Use `GET /Jobs/delivery-options`, not that list.

An option with `isCollection: true` means the customer collects; the address is still required, and the job is not shipped.

### Tokens: get one, keep it, retry once

Tokens are short-lived. Cache until just before expiry rather than per request.

Exchange your client id and secret for an access token at your region’s token endpoint, then reuse it. Requesting a fresh token per API call is the most common thing a first integration gets wrong — it is slower and it is unnecessary.

```js
// Cache with a margin
let cached = null;

async function token() {
  // A minute of margin: a token that expires mid-flight reads as a 401 you did not cause.
  if (cached && Date.now() < cached.expiresAt - 60_000) return cached.value;

  const res = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
    }),
  });

  const body = await res.json();
  cached = { value: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
  return cached.value;
}
```

> ⚠️ On a 401, drop the cached token and retry once. Retrying repeatedly with the same rejected token will not start working, and a loop against the token endpoint is how an integration gets itself rate limited.

> ⛔ Credentials belong to exactly one region and only work against that region’s host. A token minted in one region presented to another is a 401 that looks like a broken secret.

### What happens after you submit

A job moves through production. What you may still change depends on where it has got to — and the job tells you.

Poll GET /Jobs/active for everything open on the account — one request regardless of how many jobs are running, which is what our own dashboard does. Use GET /Jobs/{jobNumber} when you need the full detail of one.

Do not infer from the status what you are allowed to do. The job carries that directly: read `permissions.canEdit` before offering an edit, and `isCancelable` before offering a cancel. ⚠️ They sit at different levels — `canEdit` is nested under `permissions` (with `permissions.lockedReason` explaining a refusal), `isCancelable` is on the job itself.

> ⚠️ Those two are independent, and it is measured, not assumed: a job already in production is commonly still editable but no longer cancellable. Treating either flag as a proxy for the other produces a button that fails when the customer presses it.

Statuses are reference data, not constants. Read them from GET /Lookups/job-statuses rather than hardcoding the strings — the set changes.

A job that is not moving is usually waiting on garments rather than on the factory. `stockStatus` on GET /Jobs/{jobNumber} says so directly — `No Stock` or `Partial Stock` means the goods have not all landed, and chasing the delivery is the action, not chasing the job.

Poll on a sensible interval. Production is measured in days, so minutes between polls tells you everything a tighter loop would.

## What you can call

37 operations, every one of them callable with a customer credential. If it is
documented here, your token can use it.

- **PriceCodes** — What your account can order and what it costs. The first call in any integration — process availability and pricing are per-account, so never hardcode them.
  - `GET /PriceCodes/{priceCode}` — getPriceCodesByPriceCode
  - `GET /PriceCodes/price-codes` — getPriceCodesPricecodes
  - `GET /PriceCodes/processes` — getPriceCodesProcesses
- **Stock** — Stocked goods held on the shelf — garments, supplies and equipment — with their variants, size runs and shipping options.
  - `GET /Stock` — getStock
  - `GET /Stock/{code}` — getStockByCode
- **Jobs** — Orders. Create a job from garment and decoration lines, then track it to despatch. `validateOnly: true` dry-runs the complete payload — artwork URL included — so you can build without creating real jobs.
  - `POST /Jobs` — createJobs
  - `GET /Jobs/{jobNumber}` — getJobsByJobNumber
  - `PATCH /Jobs/{jobNumber}` — updateJobsByJobNumber
  - `POST /Jobs/{jobNumber}/cancel` — createJobsByJobNumberCancel
  - `GET /Jobs/active` — getJobsActive
  - `GET /Jobs/delivery-options` — getJobsDeliveryoptions
  - `GET /Jobs/earliest-ship-date` — getJobsEarliestshipdate
- **Inwards** — Goods you have sent in to be decorated: what was dispatched, what arrived, and whether it has been allocated to a job yet.
  - `GET /Inwards` — getInwards
  - `GET /Inwards/{id}` — getInwardsById
- **Assets** — Decoration already made for you — a digitisation, a set of screens, a separation. Reorder by asset tag: no artwork, no re-approval, no setup.
  - `GET /Assets` — getAssets
  - `GET /Assets/{assetCode}/jobs` — getAssetsByAssetCodeJobs
  - `GET /Assets/{assetTag}` — getAssetsByAssetTag
  - `GET /Assets/{assetTag}/files` — getAssetsByAssetTagFiles
  - `GET /Assets/global` — getAssetsGlobal
  - `GET /Assets/types` — getAssetsTypes
- **Boms** — Made-to-order garments, decoration included, ordered as one line by variant code. A complete integration on its own: no price codes, no artwork, no grouping.
  - `GET /Boms` — getBoms
  - `GET /Boms/{bomCode}` — getBomsByBomCode
- **Lookups** — Reference data — countries, states, job statuses, inwards statuses, process codes, size sets. Read these instead of hardcoding values that change.
  - `GET /Lookups/countries` — getLookupsCountries
  - `GET /Lookups/countries/{countryCode}/states` — getLookupsCountriesByCountryCodeStates
  - `GET /Lookups/inwards-statuses` — getLookupsInwardsstatuses
  - `GET /Lookups/job-statuses` — getLookupsJobstatuses
  - `GET /Lookups/process-codes` — getLookupsProcesscodes
  - `GET /Lookups/size-sets` — getLookupsSizesets
- **Account** — Your account: address, users, tax certificates, transactions. `GET /account` confirms which account a set of credentials belongs to.
  - `GET /account` — getAccount
  - `GET /account/shipment-settings` — getAccountShipmentsettings
  - `GET /account/transactions` — getAccountTransactions
  - `GET /account/user` — getAccountUser
  - `GET /account/users` — getAccountUsers
  - `GET /account/users/{userId}` — getAccountUsersByUserId
- **PromoCodes** — Validate a promotional code before applying it to a job.
  - `GET /promocode/validate/{code}` — getPromocodeValidateByCode
- **StockItemShipping** — Shipping options for stocked items, by stock item and market.
  - `GET /stock/{stockItemId}/shipping` — getStockByStockItemIdShipping
  - `GET /stock/shipping` — getStockShipping

## Errors

Every failure carries an RFC 7807 problem body. `detail` explains this request; on a 400 the
validation failures are in `errors` (or `validationMessages` for jobs). `401` means the token
is missing, expired, or from another region. `403` means the operation is not available to your
account's role — it will not start working on retry. `404` means no such record *or* it belongs to
another account; the two are indistinguishable by design.
