Developer documentation

Order decoration from your own system.

Every job answers two questions: what is being decorated, and what is being done to it. Send us the garments or order them from us, attach embroidery, screenprint or transfers to them, and track the whole thing to despatch — over one documented REST API.

It is the same API our dashboard runs on, not a reduced side door.

  1. 1GET /PriceCodes/price-codes

    Find what you can order, at your prices

    Availability and pricing are per-account, and each code carries the `attributes` it needs you to fill in. Never hardcode either.

  2. 2GET /Stock

    Pick the garment — if we are supplying it

    The stocked-goods feed: garments we hold on the shelf, with their variants and size runs. Skip this step when the garments are coming from you.

  3. 3POST /Jobs

    Order the garment and the decoration together

    One request, one job. A garment line carries a `groupHead`; every decoration going onto it carries the matching `group`. Send `validateOnly: true` first and nothing is created — the complete payload, artwork URL included, so what you prove is what you send.

  4. 4GET /Jobs/{jobNumber}

    Track it, including whether it is waiting on stock

    `stockStatus` answers Stock Complete / Partial Stock / No Stock, so you can tell a job the factory is holding from one it has not started.

If you are an agent

Start here.

  1. 1. Read agents.md — the guide written for you.
  2. 2. Load openapi.json — the contract, generated from the running API.
  3. 3. Never hardcode a value the API supplies. Price codes, processes, statuses, size runs, delivery methods and BOM variant codes are per-account and all change.
  4. 4. Dry-run every job with "validateOnly": true before creating it — it runs the whole validation path and creates nothing. Send the complete payload, externalArtworkUrl included: the dry run checks each artwork URL without fetching it, so creating the job changes nothing but the flag.
  5. 5. When a call fails unexpectedly, check status.json before debugging your own code.
  6. 6. The same five steps, machine-readable, live in index.json.

Always in step

Generated from the live API.

This page, the specification and the agent guide are all built from the running API rather than written alongside it, so they change when it changes. Every operation documented here works with your credentials, and every example is a real response shape.

openapi.json
OpenAPI 3.0 for codegen and tooling.
agents.md
The sequence and the rules, for coding agents.
llms-full.txt
Every endpoint, field and example in one fetch.

Getting started

How do I get credentials?

Ask your account manager. Credentials are issued per wholesale account and see only that account’s data. Self-service issuing is not available yet, so this is the one step that still involves a person.

Which host do I call?

Two, and they differ. Credentials are exchanged for a token at the auth host; every call after that goes to the API host. They are unrelated hostnames — never derive one from the other.

API host — every call
https://api.ezibrand.com.au
Auth host — tokens only
https://login.ezibrand.com.au/realms/ezibrand/protocol/openid-connect/token
Get a token
curl -X POST https://login.ezibrand.com.au/realms/ezibrand/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d client_id=$CLIENT_ID \
  -d client_secret=$CLIENT_SECRET
Use it
curl https://api.ezibrand.com.au/account \
  -H "Authorization: Bearer $TOKEN"

Build against validateOnly. Sending "validateOnly": true to POST /Jobs runs the whole validation path and creates nothing, so you can iterate on a real payload without putting anything into production. Validate the payload you intend to send, externalArtworkUrl included — the dry run checks each artwork URL’s shape and origin and never fetches it, so the only thing that changes when you create the job is the flag. ⚠️ An artwork URL that is omitted, null or empty is not checked at all, so blanking it to get a clean dry run proves nothing about the URL you then send.

Where the garment comes from

This is the first thing to settle, because it decides the shape of your job lines. Two answers, and they can be mixed in one job: a decoration attaches to a stock line and to a garment you sent in the same way. (A third thing you can order — a made-to-order garment that arrives decorated — works differently enough to have its own section below.)

01

We supply it from stock

Stocked garments and supplies, each with variants and their own size runs. A stock line takes `sizeQuantities` keyed by the sizes that variant actually has.

GET /Stock
02

You send it in

An ExternalGarment line says what the job needs, counted per size. The inwards record says what actually arrived. Filter by `?jobNumber=` to see one job’s deliveries, or `?allocated=false` for everything waiting to be matched.

POST /Jobs + GET /Inwards
03

And the decoration, second time round

A digitisation or a set of screens we have already made for you. Order the asset tag and it repeats exactly: no artwork, no re-approval, no setup charge.

GET /Assets

Ordering it again

The setup work is the expensive part: digitising a logo for embroidery, cutting screens, making a separation. Once it exists it is filed under an asset tag, and that tag is the design. Order the tag and you get the same stitch file or the same screens again — no artwork, no re-approval, and the reset fee instead of setup. It is the difference between decorating a design twice and decorating it consistently.

GET /Assets/{assetTag}

One call, and you can price a repeat at checkout without asking anyone:

  • priceBands — the quantity breaks for this design at this account’s tier. Match the quantity to a band to get the unit price. The first band’s from is the minimum orderable quantity — often 1 here, but not always, so read it rather than assuming either way.
  • reset — the repeat fee, and setup is 0. A reorder pays the reset, never setup.
  • assetUrl — a preview image, so your storefront can show the customer what they are reordering.
  • isArchived — whether the customer has retired the design. Worth warning on; it does not block an order.

⚠️ Match the quantity to a band rather than approximating to a nearby one. A quantity that falls between bands has no online price, and a line built on a guessed figure is rejected. Our own dashboard refuses it rather than inventing a number.

Browse what an account already has with GET /Assets, and the shared catalogue with GET /Assets/global.

What a tag is worth
GET /Assets/EW49123

{
  "assetTag": "EW49123",
  "description": "Riverside Rugby crest — left chest",
  "setup": 0,
  "reset": 18.50,
  "assetUrl": "https://cdn.example/assets/EW49123/preview.png",
  "isArchived": false,
  "priceBands": [
    { "from": 10,  "to": 19,   "unitPrice": 4.95 },
    { "from": 20,  "to": 49,   "unitPrice": 3.60 },
    { "from": 50,  "to": 99,   "unitPrice": 3.05 },
    { "from": 100, "to": null, "unitPrice": 2.85 }
  ]
}
Ordering it again
{
  "itemType": "Asset",
  "group": "navy-polos",
  "code": "EW49123",
  "quantity": 45,
  "customerReference": "PO-10482-2"
}

Goods you send in

When the garments are coming from you, the job line says what is needed and the inwards record says what actually turned up. They are separate because they happen separately — goods are often shipped by a supplier direct, on their own schedule.

GET /Inwards

Your deliveries, newest first and paged. Two filters do most of the work:

  • ?jobNumber= — the deliveries for one job.
  • ?allocated=false — everything not yet matched to a job. ⚠️ Allocation, not arrival: the spec defines it as job=0, so a delivery still in transit is in this list too. Read status or dateIn to know whether it has landed.

⚠️ status looks like the field to watch and is not — in practice almost every record reads Arrived. isAllocated is where the movement is.

GET /Inwards/{id} opens one delivery up: each item with its garment, colour, size set and per-size quantities. Only the non-zero sizes come back, and the run’s order lives in GET /Lookups/size-sets — not in your own sort.

What the job says it needs
{
  "itemType": "ExternalGarment",
  "groupHead": "navy-polos",
  "garment": "Navy polo — customer supplied",
  "description": "Arriving under PO-10482, 3 cartons",
  "sizeQuantities": { "S": 10, "M": 20, "L": 15 }
}
What actually arrived
GET /Inwards?jobNumber=612226

{
  "data": [
    {
      "id": 88213,
      "orderNumber": "PO-10482",
      "supplier": "Riverside Print Co",
      "cartons": 3,
      "totalQuantity": 45,
      "status": "Arrived",
      "isAllocated": true,
      "jobNumber": 612226
    }
  ],
  "page": 1, "pageSize": 25, "totalItems": 1, "totalPages": 1
}

And the job answers the other half: stockStatus on GET /Jobs/{jobNumber} reads Stock Complete, Partial Stock or No Stock across all of that job’s deliveries at once.

Made-to-order garments

Everything above builds a job out of two things: a garment, and a decoration to put on it. 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.

GET /Boms

Your catalogue, pulled on a schedule and cached. Then, per order, one line per variant sold. No price codes, no attributes, no artwork URL, no grouping.

Your SKU is already on the variant

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.

⛔ Order by the bomVariantCode you read back, never by joining the code and the size — 197 EW variants do not follow {bomCode}-{size}, so concatenating works right up until it silently orders a size that does not exist. A size of Qty means one size fits all.

⛔ The variants come with the list — GET /Boms already returns every BOM’s bomVariants, so do not loop GET /Boms/{bomCode} to collect them or you will make thousands of requests for data you already have. That endpoint is for refreshing one BOM.

⚠️ Read the delivery method from GET /Jobs/delivery-options rather than copying Ground out of the example — carriers differ by account, and a method yours does not have makes the address unroutable.

What it costs

There is no BOM price, and that is deliberate rather than missing. 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 figure on the BOM 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 in the response 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.

⚠️ Use title as the name. On that catalogue title was populated on all but one BOM and description on 54, so a listing built from the description is blank 99% of the time. And the SKU mapping only works once it is filled in — 141 BOMs carried a customerSku and 1,148 variants a customerVariantSku. Send us the mapping for the products you sell rather than assuming it is there.

The whole order
{
  "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"
  }
}
What a BOM tells you
GET /Boms/BOM10112

{
  "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 }
  ]
}

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

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.

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:

⛔ 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 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
    }
  ]
}
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
  }
}

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.

"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.

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
    }
  ]
}

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.

⛔ 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.

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

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.

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.

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.
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"
  }
}
Step 2 — create it (one character different)
{
  "validateOnly": false,
  "orderNumber": "WEB-2291",
  "…": "every other byte exactly as you validated it"
}

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.

⚠️ 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 stockStatusStock 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.

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');

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.

⛔ 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.

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

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.

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.

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 }
  ]
}

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.

⛔ 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.

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);

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.

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.

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

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.

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.

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
}

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".

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.

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"

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.

⚠️ 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.

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;
}

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.

When something fails

400
The request was rejected and nothing happened. The body names the fields that failed. Retrying it unchanged will fail identically.
401
No valid token — or a token from another region. Request a new one from the host your credentials belong to.
403
Your account’s role cannot use this operation. It will not start working on retry.
404
No such record, or it belongs to another account. The two are indistinguishable by design.
500
Usually an outage — except on POST /Jobs, where it more often means the payload is invalid. Send it again with validateOnly to see what is wrong.

To tell an outage from a bug in your own code, check /status. It reports what our monitoring sees for this region, including failures that start with the systems we depend on.

Reference

Every operation you can call, generated from the same document /developers/openapi.json serves — so this page cannot drift from the API it describes.

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}

⛔ NOT a substitute for the list when you are building a form. Measured 2026-08-28: this endpoint returns a calculated code with hasMetaDataAttributes: null and NO metadata attributes, while GET /PriceCodes/price-codes returns the same code declaring DG-X and DG-Y — so inputs built from here silently omit the dimensions the code cannot be ordered without, and nothing in the response says they are missing. Read attributes from the list; use this to price a size you already know, including the calculated codes via ?dgX=…&dgY=…. ⚠️ Percent-encode the code for the path: 20 of 25 codes on a real EW account contain spaces, so DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220 is sent as DG_DIGICAD_FLEX220%3ACad%20Cut%20Film-Flex%20220.

priceCoderequired
The Price Code to find
dgX
(optional)Needed if the PriceCode has additional MetaData requirements
dgY
(optional)Needed if the PriceCode has additional MetaData requirements
stitches
(optional)Needed if the PriceCode has additional MetaData requirements

getPriceCodesByPriceCode

Response
{
  "priceCode": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
  "description": "Cad Cut Film-Flex 220",
  "categoryCode": "DG_DIGICAD_FLEX220",
  "categoryName": "Cad Cut Film",
  "name": "Cad cut film — left chest 80mm",
  "processCode": "DG",
  "masterProcessCode": "DG",
  "minimumQuantity": 1,
  "externalSupplier": null,
  "externalPriceCode": null,
  "hasMetaDataAttributes": false,
  "standardSetup": 34.77,
  "standardReset": 20.9,
  "sampleAvailable": true,
  "samplePrice": 9.5,
  "sampleLabel": "Sample",
  "sizeUnit": null,
  "sizeWidth": null,
  "sizeHeight": null,
  "attributes": [],
  "priceBands": [
    {
      "from": 10,
      "to": 19,
      "unitPrice": 4.95
    },
    {
      "from": 20,
      "to": 49,
      "unitPrice": 3.6
    },
    {
      "from": 50,
      "to": 99,
      "unitPrice": 3.05
    },
    {
      "from": 100,
      "to": null,
      "unitPrice": 2.85
    }
  ]
}
get/PriceCodes/price-codes

Price codes for your account, with sizes and prices at your tier. The code value is what you pass as a job line code when itemType is PriceCode. Prices are yours, not list — do not cache them across accounts.

page
The page number
pageSize
The size of the Page in rows
filter
(optional)If supplied will filter the search results

getPriceCodesPricecodes

Response
[
  {
    "priceCode": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
    "description": "Cad Cut Film-Flex 220",
    "categoryCode": "DG_DIGICAD_FLEX220",
    "categoryName": "Cad Cut Film",
    "name": "Cad cut film — left chest 80mm",
    "processCode": "DG",
    "masterProcessCode": "DG",
    "minimumQuantity": 1,
    "externalSupplier": null,
    "externalPriceCode": null,
    "hasMetaDataAttributes": true,
    "standardSetup": 34.77,
    "standardReset": 20.9,
    "sampleAvailable": true,
    "sizeUnit": null,
    "sizeWidth": null,
    "sizeHeight": null,
    "attributes": [
      {
        "minLength": 0,
        "maxLength": 60,
        "name": "description",
        "type": "text",
        "label": "Description",
        "description": "Shown on the job line",
        "required": true,
        "isMetaDataAttribute": false,
        "assetAttributeName": null
      }
    ],
    "priceBands": [
      {
        "from": 10,
        "to": 19,
        "unitPrice": 4.95
      },
      {
        "from": 20,
        "to": 49,
        "unitPrice": 3.6
      },
      {
        "from": 50,
        "to": 99,
        "unitPrice": 3.05
      },
      {
        "from": 100,
        "to": null,
        "unitPrice": 2.85
      }
    ]
  }
]
get/PriceCodes/processes

The processes available to your account. Start every integration here: availability is per-account and changes, so a hardcoded process list will break for some customers and silently exclude products for others.

getPriceCodesProcesses

Response
{
  "processes": [
    {
      "processCode": "EW",
      "description": "Embroidery",
      "active": true,
      "sheetRank": 10,
      "sheetColor": "E4572E",
      "tooltip": "Embroidery",
      "mapsToMaster": "WE",
      "maximumSheetUnit": null,
      "maximumSheetWidth": null,
      "maximumSheetHeight": null
    }
  ]
}

Stock

Stocked goods held on the shelf — garments, supplies and equipment — with their variants, size runs and shipping options.

get/Stock

The stocked-goods feed: garments and supplies held on the shelf, ready to be decorated or shipped as they are. A stock line on POST /Jobs uses itemType: "Stock", the stockCode as code, a variantCode from stockVariants, and sizeQuantities keyed by the sizes in stockVariants[].sizesCsv (equipment and one-size goods use the single key Qty). Group a decoration onto it exactly as you would a garment you sent in.

page
The page number
pageSize
The size of the Page in rows
filter
(optional)If supplied will filter the search results
includePricing
stockSortColumn
stockSortDirection
variantSortColumn
variantSortDirection
markets
CW-4317: optional comma-separated list of market codes (e.g. "US,NZ"). When supplied, only stock items with at least one ACTIVE shipping configuration covering ANY of the listed markets are returned (OR semantics). Input is trimmed, uppercased, and de-duplicated. Omitted or empty => no market filter (existing behaviour). The filter is applied at the SQL level so pagination stays correct.
code
Optional exact match against a stock code or any of its variant codes. Matched exactly and not trimmed, so leading whitespace is significant; a value over the maximum code length returns 400. Combined with filter when both are supplied. Because a code is not guaranteed unique across stock codes and variant codes, this may return more than one item.
includeArchived
When true, retired stock and its variants are included. Defaults to false. Stock hidden from the API by configuration is never returned, regardless of this setting.

getStock

Response
[
  {
    "supplier": "Example Apparel Ltd",
    "categoryName": "Polos",
    "stockCode": "POL_1",
    "processCode": "STOCK",
    "stockName": "MENS PERFORMANCE POLO SHIRT",
    "stockDescription": "Polos",
    "stockSpecifications": "100% polyester pique, 160gsm.",
    "priceCompatibleLookupCode": "STOCKLOOKUP",
    "rank": 10,
    "stockVariants": [
      {
        "variantCode": "POL-1-NV.WH",
        "colour": "Navy / White",
        "variantImageUrl": "https://example.com/stock/POL-1-NV.WH/image",
        "numberOfSizes": 6,
        "sizesCsv": "S,M,L,XL,2XL,3XL"
      }
    ],
    "stockImageUrl": "https://example.com/stock/POL_1/image",
    "images": [
      {
        "url": "https://example.com/stock/POL_1/image",
        "name": null,
        "altText": null
      }
    ],
    "showAvailableStock": true,
    "markets": [
      "NZ"
    ],
    "badges": [
      "Best seller"
    ],
    "slug": "heavy-cotton-tee",
    "seoTitle": "Heavy Cotton Adult T-Shirt",
    "seoDescription": "A 180gsm cotton tee, stocked in five sizes.",
    "priceBands": [
      {
        "from": 10,
        "to": 19,
        "unitPrice": 4.95
      },
      {
        "from": 20,
        "to": 49,
        "unitPrice": 3.6
      },
      {
        "from": 50,
        "to": 99,
        "unitPrice": 3.05
      },
      {
        "from": 100,
        "to": null,
        "unitPrice": 2.85
      }
    ]
  }
]
get/Stock/{code}

One stocked item with its variants, sizes and shipping configuration. The stockVariants[].sizesCsv values are the only valid keys for a stock line's sizeQuantities.

coderequired
A stock Code or stock variant code
includePricing
variantSortColumn
variantSortDirection
includeArchived
No description.

getStockByCode

Response
{
  "supplier": "Example Apparel Ltd",
  "categoryName": "Polos",
  "stockCode": "POL_1",
  "processCode": "STOCK",
  "stockName": "MENS PERFORMANCE POLO SHIRT",
  "stockDescription": "Polos",
  "stockSpecifications": "100% polyester pique, 160gsm.",
  "priceCompatibleLookupCode": "STOCKLOOKUP",
  "rank": 10,
  "stockVariants": [
    {
      "variantCode": "POL-1-NV.WH",
      "colour": "Navy / White",
      "variantImageUrl": "https://example.com/stock/POL-1-NV.WH/image",
      "numberOfSizes": 6,
      "sizesCsv": "S,M,L,XL,2XL,3XL"
    }
  ],
  "stockImageUrl": "https://example.com/stock/POL_1/image",
  "images": [
    {
      "url": "https://example.com/stock/POL_1/image",
      "name": null,
      "altText": null
    }
  ],
  "showAvailableStock": true,
  "shippingConfigs": [
    {
      "id": 71,
      "markets": [
        "NZ"
      ],
      "shipMode": "Standard",
      "weight": null,
      "weightUnit": "kg",
      "length": null,
      "width": null,
      "height": null,
      "dimensionUnit": "cm",
      "fixedShipping": null,
      "groundOnly": false,
      "estimatedLeadDays": null,
      "fulfilmentLocations": [],
      "shipmentTypeOptions": [
        {
          "code": "Ground",
          "name": "GND",
          "price": 12.5
        }
      ]
    }
  ],
  "markets": [
    "NZ"
  ],
  "badges": [
    "Best seller"
  ],
  "slug": "heavy-cotton-tee",
  "seoTitle": "Heavy Cotton Adult T-Shirt",
  "seoDescription": "A 180gsm cotton tee, stocked in five sizes.",
  "priceBands": [
    {
      "from": 10,
      "to": 19,
      "unitPrice": 4.95
    },
    {
      "from": 20,
      "to": 49,
      "unitPrice": 3.6
    },
    {
      "from": 50,
      "to": 99,
      "unitPrice": 3.05
    },
    {
      "from": 100,
      "to": null,
      "unitPrice": 2.85
    }
  ]
}

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

Create a job. ⚠️ Send "validateOnly": true while building: the payload runs the entire validation path and returns errors without creating anything ("Validated with NO Errors. Job NOT created."). Set items[].itemType on every line — it selects the line type AND what code means: "PriceCode" (a price code, new decoration), "Asset" (an asset tag, a repeat), "Stock" (a stock code), "Bom" (a BOM code, a made-up product) or "ExternalGarment" (no code — a garment you are sending in, counted by size in sizeQuantities). Join 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, since a BOM already names its own garment and decoration. Put the artwork on each PriceCode line as externalArtworkUrl — one request places the order and delivers the art — and keep the URL reachable until the job reaches production. Include it while validating: the dry run checks it and creates nothing. ⛔ It must be an absolute https:// URL (10408 otherwise) on an origin approved for your account (10403, or 10409 where the account has none approved — both are account configuration, so ask rather than reshape the URL), and if any PriceCode line carries artwork then every one of them must (10404). Reachability is not checked during validation. A 500 from this endpoint usually means an invalid payload (empty items, unroutable address, unknown asset or BOM code, bad sizeQuantities key), not an outage. The response returns a jobNumber and, per decoration line, the asset tag it was filed under.

X-Application-Name
An Valid OriginCode value (this will be validated)

createJobs

Request
{
  "validateOnly": true,
  "orderNumber": "PO-10482",
  "description": "Riverside Rugby — club polos",
  "dateDue": "2026-09-14",
  "mustDate": false,
  "items": [
    {
      "itemType": "ExternalGarment",
      "groupHead": "navy-polos",
      "garment": "Navy polo — customer supplied",
      "description": "Arriving under PO-10482, 3 cartons",
      "customerReference": "PO-10482-G",
      "sizeQuantities": {
        "S": 10,
        "M": 20,
        "L": 15
      }
    },
    {
      "itemType": "PriceCode",
      "group": "navy-polos",
      "code": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
      "quantity": 45,
      "customerReference": "PO-10482-1",
      "attributes": {
        "description": "Club crest, left chest",
        "garment": "Navy polo",
        "Position": "Left chest 80mmW",
        "Colours": "PMS 288C, PMS 186C",
        "Size": "80mmW",
        "DG-X": 8,
        "DG-Y": 6.5
      },
      "externalArtworkUrl": "https://<an origin approved on your account>/riverside/crest.pdf"
    }
  ],
  "deliveryAddress": {
    "contactName": "Sam Patel",
    "organisation": "Riverside Print Co",
    "streetAddress": "12 Tannery Road",
    "city": "Auckland",
    "postalCode": "1010",
    "country": "New Zealand",
    "countryCodeISO2": "NZ",
    "deliveryMethod": "overnight",
    "phone": "+64 9 555 0100",
    "emailAddress": "sam@example.com"
  }
}
Response
{
  "jobNumber": 612340,
  "location": "AKL",
  "dateDue": "2026-09-14T00:00:00Z",
  "totalJobCost": 513,
  "expectingArtworkToBeUploaded": false,
  "jobLineDetails": [
    {
      "needsArtworkToBeUploaded": false,
      "customerReference": "PO-10482-1",
      "quantity": 45,
      "newAssetSku": "EW49510",
      "jobLineLabelUrl": null
    }
  ]
}
get/Jobs/{jobNumber}

Full job detail: status, money, artwork state and tracking once despatched. Status values come from GET /Lookups/job-statuses — read that list rather than matching strings you have seen before.

jobNumberrequired
A Job Number

getJobsByJobNumber

Response
{
  "customerId": 4820,
  "dateIn": "2026-08-01T02:10:00Z",
  "dateOut": null,
  "shippedDateStatus": null,
  "shippedDaysToProcess": null,
  "processingDays": 3,
  "taxTotal": 24.53,
  "isCancelable": false,
  "location": null,
  "permissions": {
    "canEdit": true,
    "lockedReason": null
  },
  "invoiceFile": null,
  "lines": [
    {
      "jobLineId": 990412,
      "assetSku": "EW49123",
      "processCode": "DG",
      "garment": "Navy polo",
      "description": "Riverside Rugby crest — left chest",
      "comments": null,
      "quantity": 45,
      "unitPrice": 3.6,
      "customerReference": "PO-10482-1",
      "imageUrl": "https://example.com/assets/EW49123/preview",
      "gang": null,
      "jobLineStatus": "In production"
    }
  ],
  "shippingAddresses": [
    {
      "trackingNumber": null,
      "trackingLink": null
    }
  ]
}
patch/Jobs/{jobNumber}

Amend a job after submitting it — the description, your PO number, the comments, the requested ship date and the must-ship flag. ⚠️ Check permissions.canEdit on GET /Jobs/{jobNumber} first: a job locks once it is dispatched, closed, returned or cancelled, and permissions.lockedReason says which. Send only the fields you are changing; anything you omit is left alone. Returns the updated job.

jobNumberrequired
The job number to update

updateJobsByJobNumber

Request
{
  "orderNumber": "PO-10482-REV2",
  "dateDue": "2026-08-21",
  "comments": "Customer asked to hold for the revised crest."
}
Response
{
  "customerId": 4820,
  "dateIn": "2026-08-01T02:10:00Z",
  "dateOut": null,
  "shippedDateStatus": null,
  "shippedDaysToProcess": null,
  "processingDays": 3,
  "taxTotal": 24.53,
  "isCancelable": true,
  "location": null,
  "permissions": {
    "canEdit": true,
    "lockedReason": null
  },
  "invoiceFile": null,
  "lines": [
    {
      "jobLineId": 990412,
      "assetSku": "EW49123",
      "processCode": "DG",
      "garment": "Navy polo",
      "description": "Riverside Rugby crest — left chest",
      "comments": null,
      "quantity": 45,
      "unitPrice": 3.6,
      "customerReference": "PO-10482-1",
      "imageUrl": "https://example.com/assets/EW49123/preview",
      "gang": null,
      "jobLineStatus": "In production"
    }
  ],
  "shippingAddresses": [
    {
      "trackingNumber": null,
      "trackingLink": null
    }
  ]
}
post/Jobs/{jobNumber}/cancel

⚠️ Check first, do not guess. GET /Jobs/{jobNumber} returns isCancelable; only call this when it is true. Once the job has entered production the answer is **409** and it cannot be undone from the API — talk to your account manager instead. Success is **204 with no body**. Cancelling an already-cancelled job succeeds rather than erroring, so a retry after a dropped connection is safe.

jobNumberrequired
The job number to cancel

createJobsByJobNumberCancel

get/Jobs/active

Every open job for the account in one call. Prefer this over looping GET /Jobs/{jobNumber}: it is one request regardless of how many jobs are open, and it is what our own dashboard polls.

excludeJobNumber
Optional job number to exclude from results
page
Page number (1-based)
pageSize
Page size (default 20, max 100)
searchText
Search by job number, order number, or description
includeClosedJobs
Include closed jobs in results. Cancelled jobs are never included
sortColumn
sortDirection
orderGroup
Optional order group to restrict results to
customerUserId
Optional Customer User (job owner) to narrow the results to - the customer's own user the job belongs to, not the Control staff member who keyed it in. Narrowing happens server-side, so totalCount, totalPages and paging describe the narrowed set. Must be a user of the calling customer; anything else is rejected with 400 rather than silently ignored.

getJobsActive

Response
{
  "items": [
    {
      "permissions": {
        "canEdit": true,
        "lockedReason": null
      },
      "jobNumber": 30291,
      "masterJobStatus": "In production",
      "originCode": "WEB",
      "description": "Riverside Rugby — club polos",
      "mustDate": false,
      "dateDue": "2026-08-14T00:00:00Z",
      "dateOut": null,
      "shippedDaysToProcess": null,
      "orderNumber": "PO-10482",
      "orderGroup": null,
      "orderGroupSequence": null,
      "tracking": []
    }
  ],
  "totalCount": 1,
  "pageNumber": 1,
  "pageSize": 25,
  "totalPages": 1,
  "hasPreviousPage": false,
  "hasNextPage": false
}
get/Jobs/delivery-options

Delivery methods your account can use. The returned method code goes in deliveryAddress.deliveryMethod on POST /Jobs. Availability varies by region and account, so read it rather than assuming a carrier.

getJobsDeliveryoptions

Response
[
  {
    "code": "Ground",
    "label": "GND",
    "isCollection": false,
    "rank": 10,
    "gangRank": 0,
    "value": "Ground",
    "deliveryLabelEnabled": false
  },
  {
    "code": "Will Collect",
    "label": "COL",
    "isCollection": true,
    "rank": 90,
    "gangRank": 0,
    "value": "Will Collect",
    "deliveryLabelEnabled": false
  }
]
get/Jobs/earliest-ship-date

The soonest despatch date, accounting for factory workload and cut-offs — so never assume a fixed lead time. Returns shippingDateUtc, shippingDateLocal, the factory timezone as an IANA name, and dateExclusions. ⛔ Take the DATE from shippingDateLocal: dateDue is a calendar date in factory-local terms, and the UTC instant lands on the previous or next day either side of midnight. ⚠️ processCodes is optional and, measured on 2026-08-29, made NO difference to the answer on this tenant — the same date came back for EW, MG, EW,MG and for a code that does not exist. Send the codes you are ordering anyway (it is the documented input and may bind on other tenants or later), but do not model per-process lead times on it, and do not treat a differing answer as impossible.

processCodes
Optional comma-separated process codes (e.g., "PR,WE")

getJobsEarliestshipdate

Response
{
  "shippingDateUtc": "2026-08-05T18:00:00+00:00",
  "shippingDateLocal": "2026-08-06T06:00:00+12:00",
  "timezone": "Pacific/Auckland",
  "dateExclusions": []
}

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

Goods you have sent in to be decorated, newest first and paged. Each row carries the order number you shipped under, the supplier, carton count, total quantity, its status, and isAllocated/jobNumber once it has been matched to a job. ⚠️ status has very little variance in practice — nearly every record reads Arrived — so isAllocated is the field that actually tells you where a delivery has got to.

allocated
false = unallocated (job=0), true = allocated (job>0)
dateFrom
Filter by delivery date from (ISO 8601)
dateTo
Filter by delivery date to (ISO 8601)
status
Filter by status name (e.g. "Arrived")
jobNumber
Return only the deliveries assigned to this job. A job belonging to another customer returns an empty page rather than an error — the endpoint does not disclose whether a job number it cannot show you exists.
page
Page number (1-based, default 1)
pageSize
Items per page (default 20, max 100)

getInwards

Response
{
  "data": [
    {
      "id": 88213,
      "orderNumber": "PO-10482",
      "supplier": "Riverside Print Co",
      "dateIn": "2026-09-02T00:00:00",
      "cartons": 3,
      "packageType": "Carton",
      "totalQuantity": 45,
      "itemCount": 1,
      "status": "Arrived",
      "isAllocated": true,
      "jobNumber": 612226,
      "description": "RRC-POLO-NAVY Polo Navy"
    },
    {
      "id": 88240,
      "orderNumber": "PO-10515",
      "supplier": "Riverside Print Co",
      "dateIn": null,
      "cartons": 1,
      "packageType": "Satchel",
      "totalQuantity": 20,
      "itemCount": 1,
      "status": "Awaiting Arrival",
      "isAllocated": false,
      "jobNumber": null,
      "description": "RRC-TEE-WHITE Tee White"
    }
  ],
  "page": 1,
  "pageSize": 25,
  "totalItems": 2,
  "totalPages": 1
}
get/Inwards/{id}

One delivery in full: every item with its garment, colour, size set and per-size quantities — what actually arrived, against what was expected. The size set names an ordered run from GET /Lookups/size-sets; a line returns only its non-zero sizes, so read the order from the size set rather than from the item.

idrequired
The delivery id, as returned by GET /Inwards. Not your order number.

getInwardsById

Response
{
  "supplierId": 4412,
  "dateDue": "2026-09-02T00:00:00",
  "shelfLocation": "B14",
  "comment": "Left of the roller door.",
  "packingSlipNumber": "ORN0073086",
  "items": [
    {
      "id": 130551,
      "code": "RRC-POLO-NAVY",
      "garment": "Polo",
      "colour": "Navy",
      "sizeSetId": 12,
      "sizeSet": "Adult S-3XL",
      "quantities": [
        {
          "size": "S",
          "qty": 10
        },
        {
          "size": "M",
          "qty": 20
        },
        {
          "size": "L",
          "qty": 15
        }
      ],
      "total": 45
    }
  ]
}

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

Prints already made for your account. Each carries an asset tag; ordering that tag again reproduces the previous run exactly, with no artwork upload and no colour re-approval.

page
The page number
pageSize
The size of the Page in rows
sortColumn
Column to sort by
sortDirection
Sort direction (ascending or descending)
includeProcesses
(optional)If passed, can contain a list of Process codes to include (comma separated)
excludeProcesses
(optional)If passed, can contain a list of Process codes to exclude (comma separated)
filter
(optional)If passed will filter the search using the filter text
isArchived
(optional)If true, returns only archived assets. If false, only non-archived. If omitted, returns both.
priceCodeContains
(optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains ANY of them. NULL PriceCodes are excluded.
priceCodeNotContains
(optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains NONE of them, including assets with NULL PriceCode.

getAssets

Response
{
  "pageSize": 25,
  "returnedResults": 1,
  "totalResults": 42,
  "totalPages": 1,
  "currentPage": 1,
  "hasNext": false,
  "hasPrevious": false,
  "filter": null,
  "nextUrl": null,
  "previousUrl": null,
  "entities": [
    {
      "assetId": 774301,
      "assetTag": "EW49123",
      "processCode": "DG",
      "description": "Riverside Rugby crest — left chest",
      "garment": "Navy polo",
      "priceTierCode": "B",
      "setup": 0,
      "reset": 18.5,
      "priceCode": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
      "assetUrl": "https://example.com/assets/EW49123/preview",
      "createDate": "2026-06-18T22:41:05Z",
      "isGlobal": false,
      "isArchived": false,
      "priceBands": [
        {
          "from": 10,
          "to": 19,
          "unitPrice": 4.95
        },
        {
          "from": 20,
          "to": 49,
          "unitPrice": 3.6
        },
        {
          "from": 50,
          "to": 99,
          "unitPrice": 3.05
        },
        {
          "from": 100,
          "to": null,
          "unitPrice": 2.85
        }
      ]
    }
  ]
}
get/Assets/{assetCode}/jobs

Returns both active and closed jobs for the authenticated customer only. Customer ID is extracted from the authentication context. Filters by Job's customer (CustID), not Asset's ClientID. Sorting: Active jobs (dateOut=null) appear FIRST at the top, followed by completed/shipped jobs sorted by dateOut descending (most recent first). Used by SC Integrate to display Order History on asset detail pages.

assetCoderequired
The asset tag, as returned by GET /Assets.

getAssetsByAssetCodeJobs

Response
{
  "assetCode": "EW49123",
  "jobs": [],
  "totalCount": 0
}
get/Assets/{assetTag}

One asset by its tag. Use it to read the reset charge and price bands a reorder will be billed at, and to see whether the customer has retired the design — isArchived. ⚠️ Archived is a warning to surface, not a gate: an order referencing an archived asset is still accepted.

assetTagrequired
The asset tag, as returned by GET /Assets — the identifier of a decoration we have already made for you.

getAssetsByAssetTag

Response
{
  "assetId": 774301,
  "assetTag": "EW49123",
  "processCode": "DG",
  "description": "Riverside Rugby crest — left chest",
  "garment": "Navy polo",
  "priceTierCode": "B",
  "setup": 0,
  "reset": 18.5,
  "priceCode": "DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220",
  "assetUrl": "https://example.com/assets/EW49123/preview",
  "createDate": "2026-06-18T22:41:05Z",
  "isGlobal": false,
  "isArchived": false,
  "priceBands": [
    {
      "from": 10,
      "to": 19,
      "unitPrice": 4.95
    },
    {
      "from": 20,
      "to": 49,
      "unitPrice": 3.6
    },
    {
      "from": 50,
      "to": 99,
      "unitPrice": 3.05
    },
    {
      "from": 100,
      "to": null,
      "unitPrice": 2.85
    }
  ]
}
get/Assets/{assetTag}/files

The artwork files held against an asset, current and superseded. historicFiles records what was replaced and when.

assetTagrequired
The asset tag, as returned by GET /Assets.

getAssetsByAssetTagFiles

Response
{
  "assetTag": "EW49123",
  "currentFiles": [],
  "historicFiles": []
}
get/Assets/global

Catalogue assets available to every account, rather than ones your account created. Order them exactly like your own: itemType: "Asset" with the tag as code.

page
The page number
pageSize
The size of the Page in rows
sortColumn
Column to sort by
sortDirection
Sort direction (ascending or descending)
includeProcesses
(optional)If passed, can contain a list of Process codes to include (comma separated)
excludeProcesses
(optional)If passed, can contain a list of Process codes to exclude (comma separated)
filter
(optional)If passed will filter the search using the filter text
isArchived
(optional)If true, returns only archived assets. If false, only non-archived. If omitted, returns both.
priceCodeContains
(optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains ANY of them. NULL PriceCodes are excluded.
priceCodeNotContains
(optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains NONE of them, including assets with NULL PriceCode.

getAssetsGlobal

Response
{
  "pageSize": 25,
  "returnedResults": 1,
  "totalResults": 1,
  "totalPages": 1,
  "currentPage": 1,
  "hasNext": false,
  "hasPrevious": false,
  "filter": null,
  "nextUrl": null,
  "previousUrl": null,
  "entities": [
    {
      "assetId": 5012,
      "assetTag": "GL00042",
      "processCode": "STOCK",
      "description": "Care label",
      "garment": null,
      "priceTierCode": "",
      "setup": 0,
      "reset": 0,
      "priceCode": null,
      "assetUrl": null,
      "createDate": "2025-11-02T03:15:00Z",
      "isGlobal": true,
      "isArchived": false,
      "priceBands": [
        {
          "from": 1,
          "to": null,
          "unitPrice": 0.35
        }
      ]
    }
  ]
}
get/Assets/types

Which asset type each process produces. Useful for labelling assets in your own UI without hardcoding a mapping that changes.

getAssetsTypes

Response
[
  {
    "process": "EW",
    "assetType": "Embroidery"
  },
  {
    "process": "MG",
    "assetType": "Direct Screen"
  },
  {
    "process": "DG",
    "assetType": "Digital Transfers"
  }
]

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

The made-up products we can supply you: a garment with its decoration already specified, ordered as one unit. ⚠️ Measured on 2026-08-28: this returns EVERY BOM in one ~4 MB unpaged response (5,724 of them on a real account). Pull it on a schedule and cache it — never per order, and never per page render.

filter
Substring matched against the BOM code or any variant code.
includeItems
When true, returns each BOM's composition on items. Opt-in because it is a second query and fans the response out across every item of every BOM returned.
includeArchived
When true, archived (inactive) BOMs are included, along with their inactive variants. Hard-deleted BOMs are never returned. Defaults to false.

getBoms

Response
[
  {
    "id": 41207,
    "bomCode": "BOM10112",
    "title": "Riverside Rugby — club polo, navy",
    "description": "Navy pique polo, embroidered club crest left chest.",
    "range": "Riverside Rugby",
    "customerSku": "RRC-POLO-NAVY",
    "bomVariants": [
      {
        "bomVariantCode": "BOM10112-S",
        "size": "S",
        "customerVariantSku": "RRC-POLO-NAVY-S"
      },
      {
        "bomVariantCode": "BOM10112-M",
        "size": "M",
        "customerVariantSku": "RRC-POLO-NAVY-M"
      },
      {
        "bomVariantCode": "BOM10112-72R",
        "size": "72",
        "customerVariantSku": null
      }
    ]
  },
  {
    "id": 41208,
    "bomCode": "BOM10113",
    "title": "Riverside Rugby — kit bag",
    "description": "Team kit bag, printed club crest.",
    "range": "Riverside Rugby",
    "customerSku": null,
    "bomVariants": [
      {
        "bomVariantCode": "BOM10113-QTY",
        "size": "Qty",
        "customerVariantSku": null
      }
    ]
  }
]
get/Boms/{bomCode}

One BOM with its variants — one variant per size, each with its own bomVariantCode and your own customerVariantSku if you have set one. ⛔ Order by the bomVariantCode you read here. It is *usually* {bomCode}-{size} and 197 EW variants are not, so building it by concatenation works until it silently orders a size that does not exist. On the job line, customName carries a per-item personalisation (a back name); leave it out and the BOM explodes as usual.

bomCoderequired
The BOM code, as returned by GET /Boms — the product, not one of its size variants. ⛔ Order by the bomVariantCode this call returns, never by joining the code and a size.
includeItems
When true, returns the BOM's composition on items — the garment plus its branding lines, each with its asset code, process code and position on the garment. Omitted (null) by default so the default response shape is unchanged.
includeArchived
When true, an archived (inactive) BOM is returned, and its inactive variants are included. Hard-deleted BOMs are never returned. Defaults to false.

getBomsByBomCode

Response
{
  "id": 41207,
  "bomCode": "BOM10112",
  "title": "Riverside Rugby — club polo, navy",
  "description": "Navy pique polo, embroidered club crest left chest.",
  "range": "Riverside Rugby",
  "customerSku": "RRC-POLO-NAVY",
  "bomVariants": [
    {
      "bomVariantCode": "BOM10112-S",
      "size": "S",
      "customerVariantSku": "RRC-POLO-NAVY-S"
    },
    {
      "bomVariantCode": "BOM10112-M",
      "size": "M",
      "customerVariantSku": "RRC-POLO-NAVY-M"
    },
    {
      "bomVariantCode": "BOM10112-L",
      "size": "L",
      "customerVariantSku": "RRC-POLO-NAVY-L"
    },
    {
      "bomVariantCode": "BOM10112-72R",
      "size": "72",
      "customerVariantSku": null
    }
  ]
}

Lookups

Reference data — countries, states, job statuses, inwards statuses, process codes, size sets. Read these instead of hardcoding values that change.

get/Lookups/countries

Returns a list of countries available for address selection, along with the tenant's default country.

getLookupsCountries

Response
{
  "countries": [
    {
      "name": "New Zealand",
      "iso2": "NZ",
      "iso3": "NZL",
      "hasSuburb": true,
      "hasState": false,
      "hasPostalCode": true
    },
    {
      "name": "United States",
      "iso2": "US",
      "iso3": "USA",
      "hasSuburb": false,
      "hasState": true,
      "hasPostalCode": true
    }
  ],
  "defaultCountry": "NZ"
}
get/Lookups/countries/{countryCode}/states

Returns a list of states/provinces/regions for the specified country code. Some countries may return an empty list if they don't have administrative divisions.

countryCoderequired
ISO 3166-1 alpha-2 country code (e.g., US, NZ, GB)

getLookupsCountriesByCountryCodeStates

Response
{
  "countryCode": "US",
  "states": []
}
get/Lookups/inwards-statuses

Every status an inwards delivery can hold. Read this rather than hardcoding, for the same reason as job statuses. ⚠️ To know whether a JOB is waiting on stock, read stockStatus on GET /Jobs/{jobNumber} instead — it answers Stock Complete / Partial Stock / No Stock across all of that job's deliveries at once.

getLookupsInwardsstatuses

Response
[
  {
    "id": 1,
    "name": ""
  },
  {
    "id": 2,
    "name": "Cart"
  },
  {
    "id": 3,
    "name": "Awaiting Arrival"
  },
  {
    "id": 4,
    "name": "Arrived"
  }
]
get/Lookups/job-statuses

The full set of job statuses. Read this rather than hardcoding: statuses are added over time, and an unrecognised status should never break your integration.

getLookupsJobstatuses

Response
[
  {
    "id": 1,
    "name": "Awaiting artwork"
  },
  {
    "id": 2,
    "name": "In production"
  },
  {
    "id": 3,
    "name": "Shipped"
  }
]
get/Lookups/process-codes

Returns a list of manufacturing process codes available to you, ordered by rank (ascending).

getLookupsProcesscodes

Response
[
  {
    "processCode": "EW",
    "description": "Embroidery",
    "rank": 10,
    "metadata": {
      "setupApplies": true,
      "resetApplies": false,
      "scheduleable": true,
      "physicalStockRequired": false,
      "printable": true
    }
  }
]
get/Lookups/size-sets

The named size runs used by stock lines, BOM variants and inwards records — 300 of them, each an ORDERED list of size codes. ⛔ Read both the sizes and their order from here. Sorting them yourself puts 10 before 2 and XL before XS, and a size run displayed out of order is a mis-picked order.

getLookupsSizesets

Response
[
  {
    "id": 3,
    "name": "Adult",
    "sizes": [
      "S",
      "M",
      "L",
      "XL",
      "2XL"
    ]
  }
]

Account

Your account: address, users, tax certificates, transactions. GET /account confirms which account a set of credentials belongs to.

get/account

The account these credentials belong to. Call it once a token works to confirm you are pointed at the right customer. (It cannot tell you which region to use — you need the right regional host to get a token at all, and that host comes with your credentials.)

getAccount

Response
{
  "id": 4820,
  "name": "Riverside Print Co",
  "priceTierCode": "B",
  "currency": "NZD",
  "taxRateType": "Inclusive",
  "taxRate": 15,
  "taxSystemTaxExempt": false,
  "taxSystemTaxExemptReason": "",
  "showPayNow": true,
  "creditCardRequired": false,
  "shipmentCarriersCSV": "",
  "excludeShipmentsCsv": null,
  "courierAccountCode": "",
  "courierSiteID": null,
  "crmId": "",
  "accountManagerName": "Alex Rivera",
  "accountManagerEmail": "alex@example.com",
  "tierLast12Months": "B",
  "paymentTermDays": 20,
  "paymentTermType": "FollowingMonthEnd",
  "entityAddress": {
    "id": 90211,
    "streetAddress": "12 Tannery Road",
    "addressLine2": "",
    "city": "Auckland",
    "state": "",
    "stateFull": "",
    "postalCode": "1010",
    "countryCode": "NZ",
    "countryName": "New Zealand",
    "contactName": "Sam Patel",
    "organisation": "Riverside Print Co",
    "phone": "+64 9 555 0100",
    "emailAddress": "sam@example.com",
    "addressSummaryOneLine": "12 Tannery Road, Auckland 1010, New Zealand"
  },
  "taxCertificates": [],
  "paymentMode": null,
  "paymentMethods": []
}
get/account/shipment-settings

Retrieves shipment configuration for your organization.

getAccountShipmentsettings

Response
{
  "availableCarriers": [
    "NZC01"
  ],
  "carrierOptions": [
    "NZC01"
  ],
  "receiverPays": {
    "billingAccountNumber": "",
    "billingPostalCode": "",
    "billingCountryCode": ""
  },
  "showCustomerAddressOnLabel": true,
  "handlingFee": 0,
  "applyHandlingFeeInsteadOfFreight": false,
  "jobComment": "",
  "availableShipmentTypes": [
    "Ground"
  ],
  "shipmentTypeOptions": [
    {
      "code": "Ground",
      "name": "GND"
    }
  ],
  "freightRates": [
    {
      "carrier": "NZC01",
      "carrierName": "Courier",
      "service": null,
      "serviceName": null,
      "package": null,
      "packageName": null,
      "chargeType": "FlatRatePerJob",
      "value": 12.5,
      "minimum": null,
      "isDefault": true
    }
  ]
}
get/account/transactions

Retrieves a paginated list of payment transactions for the authenticated customer. Includes card details (masked), amounts, and transaction status.

page
Page number, 1-based (default: 1)
pageSize
Number of records per page (default: 20)
sortBy
Column to sort by (default: Id). Valid values: Id, CreatedAt, CompletedAt, AmountSettlement
sortDirection
Sort direction: ASC or DESC (default: DESC)
completedOnly
Filter to show only completed transactions (default: false)
textFilter
Optional text filter to search transaction details

getAccountTransactions

Response
{
  "pageSize": 25,
  "returnedResults": 0,
  "totalResults": 0,
  "totalPages": 1,
  "currentPage": 1,
  "hasNext": false,
  "hasPrevious": false,
  "filter": null,
  "nextUrl": null,
  "previousUrl": null,
  "entities": []
}
get/account/user

Retrieves your user profile information including contact details, preferences, and notification settings.

getAccountUser

Response
{
  "id": 51188,
  "guid": "3f7c1e28-0000-4a1b-9c44-2b6d5e900001",
  "customerId": 4820,
  "customerName": "Riverside Print Co",
  "userName": "spatel",
  "firstName": "Sam",
  "lastName": "Patel",
  "emailAddress": "sam@example.com",
  "active": true,
  "phone": "+64 9 555 0100",
  "mobile": "",
  "streetAddress": "12 Tannery Road",
  "addressLine2": "",
  "suburb": "",
  "city": "Auckland",
  "state": "",
  "postalCode": "1010",
  "country": "New Zealand",
  "locationID": null,
  "locationName": null,
  "defaultPage": "",
  "orderNumberPrefix": "",
  "receiveEmails": true,
  "promotionRecipient": false,
  "receiveInvoice": true,
  "receiveInwards": false,
  "textNotifyProof": false,
  "textNotifyDispatch": false,
  "usernameLoginOnly": false,
  "lastLogin": "2026-08-01T09:14:22Z",
  "loginCount": 214
}
get/account/users

Retrieves a paginated list of all users in your organization. Only available to customer administrators. Supports filtering by active status, text search, sorting, and pagination.

includeInactive
Include inactive users (default: false)
page
Page number, 1-based (default: 1)
pageSize
Number of records per page (default: 50)
sortBy
Column to sort by (default: LastName). Valid values: LastName, FirstName, UserName, Email, LastLoginUtc, LoginCount
sortDirection
Sort direction: ASC or DESC (default: ASC)
textFilter
Optional text filter to search username, first name, last name, or email

getAccountUsers

Response
{
  "pageSize": 25,
  "returnedResults": 1,
  "totalResults": 1,
  "totalPages": 1,
  "currentPage": 1,
  "hasNext": false,
  "hasPrevious": false,
  "filter": null,
  "nextUrl": null,
  "previousUrl": null,
  "entities": [
    {
      "id": 51188,
      "guid": "3f7c1e28-0000-4a1b-9c44-2b6d5e900001",
      "userName": "spatel",
      "firstName": "Sam",
      "lastName": "Patel",
      "emailAddress": "sam@example.com",
      "active": true,
      "locationName": null,
      "usernameLoginOnly": false,
      "lastLogin": "2026-08-01T09:14:22Z",
      "loginCount": 214
    }
  ]
}
get/account/users/{userId}

One user on your account. Requires the account administrator role; an ordinary user credential receives a 403.

userIdrequired
User ID to retrieve

getAccountUsersByUserId

Response
{
  "id": 51188,
  "guid": "3f7c1e28-0000-4a1b-9c44-2b6d5e900001",
  "userName": "spatel",
  "firstName": "Sam",
  "lastName": "Patel",
  "emailAddress": "sam@example.com",
  "active": true,
  "locationName": null,
  "usernameLoginOnly": false,
  "lastLogin": "2026-08-01T09:14:22Z",
  "loginCount": 214
}

PromoCodes

Validate a promotional code before applying it to a job.

get/promocode/validate/{code}

Validates a promo code for the authenticated customer. Checks if the code exists, is active, not expired, and hasn't exceeded usage limits (overall or per-customer). Optionally validates that the promo code is applicable to specific process codes. Returns: - isValid: Whether the promo code can be used - promoCode: The promo code that was validated - discount: Discount amount (cents if isFixed=true, percentage if isFixed=false) - isFixed: true for fixed amount discount, false for percentage discount - message: Error message if not valid (expired, max uses reached, not applicable to process, etc.)

coderequired
Promo code to validate
processCodes
Optional comma-separated list of process codes (e.g., "Embroidery,Printing") from ProcessRank.Process

getPromocodeValidateByCode

Response
{
  "isValid": true,
  "promoCode": "WINTER10",
  "description": null,
  "discount": 10,
  "isFixed": false,
  "message": "Promotion applied successfully.",
  "applicableProcessCodes": null
}

StockItemShipping

Shipping options for stocked items, by stock item and market.

get/stock/{stockItemId}/shipping

How one stocked item ships — weight, dimensions, lead time and which warehouses it can ship from.

stockItemIdrequired
The stock item ID

getStockByStockItemIdShipping

Response
[
  {
    "id": 71,
    "stockItemId": 545,
    "markets": [
      "NZ"
    ],
    "shipMode": "ShipsSeparately",
    "fulfilmentLocations": [
      {
        "locationCode": "AKL",
        "locationName": "Auckland",
        "locale": "NZ",
        "shipFromCity": "Auckland",
        "shipFromCountryCodeIso2": "NZ"
      }
    ],
    "weight": 0.25,
    "weightUnit": "kg",
    "length": 30,
    "width": 22,
    "height": 4,
    "dimensionUnit": "cm",
    "fixedShipping": null,
    "groundOnly": false,
    "estimatedLeadDays": 2,
    "active": true,
    "createdAt": "2026-03-11T20:00:00Z",
    "updatedAt": "2026-07-02T04:30:00Z",
    "shipmentTypeOptions": []
  }
]
get/stock/shipping

Shipping options for stocked items. ⚠️ The market query parameter is required even though the schema does not mark it required — omitting it returns a 400.

market
The market code (e.g., US, AU, NZ, UK)

getStockShipping

Response
[
  {
    "id": 71,
    "stockItemId": 545,
    "markets": [
      "NZ"
    ],
    "shipMode": "ShipsSeparately",
    "fulfilmentLocations": [
      {
        "locationCode": "AKL",
        "locationName": "Auckland",
        "locale": "NZ",
        "shipFromCity": "Auckland",
        "shipFromCountryCodeIso2": "NZ"
      }
    ],
    "weight": 0.25,
    "weightUnit": "kg",
    "length": 30,
    "width": 22,
    "height": 4,
    "dimensionUnit": "cm",
    "fixedShipping": null,
    "groundOnly": false,
    "estimatedLeadDays": 2,
    "active": true,
    "createdAt": "2026-03-11T20:00:00Z",
    "updatedAt": "2026-07-02T04:30:00Z",
    "shipmentTypeOptions": []
  }
]