# 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 ` 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": "", "quantity": 45, "externalArtworkUrl": "https:///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": "", "variantCode": "", "sizeQuantities": { "S": 10, "M": 20, "L": 15 } }, { "itemType": "PriceCode", "group": "navy-polos", "code": "", "quantity": 45, "customerReference": "WEB-2291-1", "attributes": { "description": "Club crest", "Position": "Left chest" }, "externalArtworkUrl": "https:///riverside/crest.pdf" }, { "itemType": "Asset", "group": "navy-polos", "code": "", "quantity": 45, "customerReference": "WEB-2291-2" } ], "deliveryAddress": { "contactName": "Sam Patel", "streetAddress": "12 Tannery Road", "city": "Auckland", "postalCode": "1010", "countryCodeISO2": "NZ", "deliveryMethod": "", "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. --- # Full reference ## 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} **operationId:** `getPriceCodesByPriceCode` **Summary:** Gets the priceCodes for the current Customer ⛔ 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`. **Parameters** - `priceCode` _(required)_ — in `path`: The Price Code to find - `dgX` — in `query`: (optional)Needed if the PriceCode has additional MetaData requirements - `dgY` — in `query`: (optional)Needed if the PriceCode has additional MetaData requirements - `stitches` — in `query`: (optional)Needed if the PriceCode has additional MetaData requirements **Responses** - **200** — Success _One price code, with its attributes and quantity breaks_ ```json { "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 } ] } ``` - **400** — Bad Request — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **404** — Not Found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. #### Schema: `PriceCodeDto` Defines a Price Code that can be used to create new Jobs - `priceCode` (string, nullable): The price code itself — pass this verbatim as a job line `code` when `itemType` is `"PriceCode"`. ⚠️ The size inside the string (`…-4" x 4"`) is part of the code's NAME, not a measurement. Never parse it to compute artwork dimensions; read `sizeWidth`/`sizeHeight`/`sizeUnit` instead. - `description` (string, nullable): price code description - `categoryCode` (string, nullable): The Category Code that the PriceCode belongs to - `categoryName` (string, nullable): The Category Name that the PriceCode belongs to - `name` (string, nullable): The name of the item for this Price Code - `processCode` (string, nullable): The Process Code for the Price code - `masterProcessCode` (string, nullable): The Master Process Code for the Price code - this maps to the Asset Type code - `minimumQuantity` (integer): The fewest units this code can be ordered in. A job line below it is rejected. - `externalSupplier` (string, nullable): Indicates that this PriceCode is supplied Externally - `externalPriceCode` (string, nullable): The External Price Code for the Price Code - `hasMetaDataAttributes` (boolean): ⚠️ Whether this code is calculated — priced from inputs rather than a fixed size. MEASURED INCONSISTENCY: `GET /PriceCodes/price-codes` returns `true` for a calculated code while `GET /PriceCodes/{priceCode}` returns null for the SAME code and omits its metadata attributes entirely. Build input forms from the LIST. - `standardSetup` (number, nullable): The cost to setup for this asset job ($) - if outputs as NULL then the Setup DOES NOT APPLY - `standardReset` (number, nullable): The reset cost to for this asset job ($) - if outputs as NULL then the Setup DOES NOT APPLY - `sampleAvailable` (boolean): Gets or sets a value indicating whether the same resource is available for use. - `samplePrice` (number, nullable): Sample value that outputs conditionally based on ShouldSerializeSample - if outputs as NULL then the Sample IS NOT AVAILABLE, and will be removed as a return field - `sampleLabel` (string, nullable): Gets or sets the label associated with the sample - if outputs as NULL then the Sample IS NOT AVAILABLE, and will be removed as a return field - `sizeUnit` (string, nullable): The unit of `sizeWidth`/`sizeHeight`, and the unit any `DG-X`/`DG-Y` you send must be in. It is a property of THIS PRICE CODE — never of the region, and never of another code in the same process. ⛔ NULL means unknown, not a default: it is populated only where the size label can be parsed, so it is absent on exactly the calculated codes whose `DG-X`/`DG-Y` need it. Where it is null, ask us rather than assuming millimetres — assuming misquotes by 10× per axis, 100× on area. - `sizeWidth` (number, nullable): Width as a number, in `sizeUnit`. Helper data derived from the code’s own size label; the label string itself is never modified. - `sizeHeight` (number, nullable): Height as a number, in `sizeUnit`. - `categoryDescription` (string, nullable): The tenant-authored description of the price category this Price Code belongs to (sourced from Category.PriceDescription) - `categoryMetadata` (string, nullable): An opaque tenant-defined JSON string describing the category. Control stores and serves this verbatim and never interprets it - parse it client-side. - `unitType` (string, nullable): The category unit as a first-class value. Previously this was only available embedded in the composite priceCode string. - `attributes` (array, nullable): Any attributes available for the Price code (note: some may be required when being used) - `priceBands` (array, nullable): The quantity-break ladder at YOUR account's tier. Prices are per-tier, so never cache them across accounts or show them to another customer. #### Schema: `ProcessAttribute` Process Attribute base Type identifies the type of attribute - `name` (string, nullable): The key to send this answer back under, on the job line’s `attributes` object. Case-sensitive, and per price code — never hardcode the set. - `type` (string, nullable): How to render the input: `text` bounded by `maxLength`, `select` over `enumerableValues`, `decimal` as a number. Branch on this — the subtypes carry different fields. - `label` (string, nullable): Label - this would be used to show a label for a screen field - `description` (string, nullable): ⚠️ A hint for the human, and NOT a source of truth for units. The `DG-X` description reads “In centimetres” in every region including ones where that is wrong; read `sizeUnit` instead. - `required` (boolean): Indicates the attribute value MUST be passed - `isMetaDataAttribute` (boolean): ⛔ `true` means this is not a question for a human: it is an input to the PRICE. `DG-X` and `DG-Y` are the width and height of a cut-to-size product, and their unit is the price code’s `sizeUnit` — which is null on exactly these codes, so ask rather than assume. Pass them to `GET /PriceCodes/{priceCode}?dgX=…&dgY=…` to price a size. - `enumerableValues` (array, nullable): The permitted values when this attribute is a choice. Read them from here rather than hardcoding — they change per account and per process. - `assetAttributeName` (string, nullable): Determines that the element must be saved to the AssetAttribute if provided #### Schema: `ProcessNumericAttributeOf1T` Text attribute for Process - `minValue` (object, nullable): Minimum value for number - `maxValue` (object, nullable): max number for number value #### Schema: `ProcessTextAttribute` Text attribute for Process - `minLength` (integer): Min length of string - `maxLength` (integer): max length of string #### Schema: `IPriceBandDto` - `from` (integer): Lowest quantity this band applies to, inclusive. - `to` (integer, nullable): Highest quantity this band applies to, inclusive. Null on the top band, which has no upper limit. - `unitPrice` (number): Price per unit within this band, in the account's currency. ⚠️ Per-tier: this is your account's price, not a list price. #### Schema: `ProblemDetails` - `type` (string, nullable): URI identifying the problem type (RFC 7807). Stable enough to branch on; the human-readable part is `title`. - `title` (string, nullable): Short summary of the problem, the same for every occurrence of this type. - `status` (integer, nullable): The HTTP status code, repeated in the body so it survives logging. - `detail` (string, nullable): What went wrong on THIS request. The most useful field for a human reading a failure. - `instance` (string, nullable): The path that produced the problem. #### Schema: `HttpValidationProblemDetails` - `errors` (object, nullable): Validation failures keyed by the field that caused them, each with one or more messages. Read this rather than the top-level `detail` when the status is 400. ### GET /PriceCodes/price-codes **operationId:** `getPriceCodesPricecodes` **Summary:** Gets the priceCodes for the current Customer 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. **Parameters** - `page` — in `query`: The page number - `pageSize` — in `query`: The size of the Page in rows - `filter` — in `query`: (optional)If supplied will filter the search results **Responses** - **200** — Success _Price codes for this account, at this account’s tier_ ```json [ { "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 } ] } ] ``` - **400** — Bad Request — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. ### GET /PriceCodes/processes **operationId:** `getPriceCodesProcesses` **Summary:** Get list of pricing processes from the Pricing database 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. **Responses** - **200** — Successfully retrieved pricing processes _Processes this account can order — start every integration here_ ```json { "processes": [ { "processCode": "EW", "description": "Embroidery", "active": true, "sheetRank": 10, "sheetColor": "E4572E", "tooltip": "Embroidery", "mapsToMaster": "WE", "maximumSheetUnit": null, "maximumSheetWidth": null, "maximumSheetHeight": null } ] } ``` - **401** — Authentication required — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer `. A token from another region’s host will also produce this. - **403** — Access denied - user lacks required permissions — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **500** — An error occurred — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `PricingProcessesResponse` Response wrapper for pricing processes - `processes` (array, nullable): List of pricing processes #### Schema: `PricingProcessDto` Pricing process information from the Pricing database - `processCode` (string, nullable): Process code identifier - `description` (string, nullable): Human-readable process description - `active` (boolean): Whether process is currently available - `sheetRank` (integer): Sort order for UI display - `sheetColor` (string, nullable): Hex color code for UI display - `tooltip` (string, nullable): Tooltip text for UI - `mapsToMaster` (string, nullable): Master process code for grouping variants - `maximumSheetUnit` (string, nullable): Unit of measure for the maximum sheet dimensions (cm / in / mm) - `maximumSheetWidth` (number, nullable): Maximum sheet width in Codewolf.Ctrl.Api.Dtos.Api.PricingProcessDto.MaximumSheetUnit - `maximumSheetHeight` (number, nullable): Maximum sheet height in Codewolf.Ctrl.Api.Dtos.Api.PricingProcessDto.MaximumSheetUnit - `metadata` (string, nullable): An opaque tenant-defined JSON string describing the process. Control stores and serves this verbatim and never interprets it - parse it client-side. ## Stock Stocked goods held on the shelf — garments, supplies and equipment — with their variants, size runs and shipping options. ### GET /Stock **operationId:** `getStock` **Summary:** Get a List of stock and its variants 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. **Parameters** - `page` — in `query`: The page number - `pageSize` — in `query`: The size of the Page in rows - `filter` — in `query`: (optional)If supplied will filter the search results - `includePricing` — in `query`: - `stockSortColumn` — in `query`: - `stockSortDirection` — in `query`: - `variantSortColumn` — in `query`: - `variantSortDirection` — in `query`: - `markets` — in `query`: 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` — in `query`: 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` — in `query`: 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. **Responses** - **200** — Success _The stocked-goods catalogue_ ```json [ { "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 } ] } ] ``` - **400** — Bad Request — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. #### Schema: `StockDto` Information about an available Stock - `supplier` (string, nullable): Stock Supplier - `categoryName` (string, nullable): Stock Category - `categoryUnitId` (string, nullable): Stock Category Unit ID (for Pricing) - `stockItemId` (integer): Internal StockItemId used to enrich list responses with per-item data (e.g. markets from StockItemShipping). Never serialised to API consumers. - `stockCode` (string, nullable): The code which identifies the Stock - `processCode` (string, nullable): the Process Code - `stockName` (string, nullable): Stock name - `stockDescription` (string, nullable): Gets or sets the description of the stock item. - `stockSpecifications` (string, nullable): Gets or sets the specifications for the stock item. - `priceCompatibleLookupCode` (string, nullable): The Price Compatible Lookup Code - `rank` (integer): Gets or sets the rank or position associated with the current instance. - `stockVariants` (array, nullable): The stock Variants as a list - `stockImageUrl` (string, nullable): Gets or sets the URL or file path of the hero stock image associated with the item. Backwards compatible field — mirrors `Images[0]?.Url` when the gallery is non-empty. - `images` (array, nullable): CW-4346: ordered gallery of images for this stock item. Index 0 is the hero (matches Codewolf.Ctrl.Common.Classes.Stocks.StockDto.StockImageUrl); subsequent entries are additional gallery images. Empty list when the stock item has no images. Always serialised. - `showAvailableStock` (boolean): Gets or sets a value indicating whether the available stock should be displayed to users. - `shippingConfigs` (array, nullable): Per-market shipping configurations for this stock item. Only populated on single stock item endpoint (GET /stock/{code}). - `markets` (array, nullable): Distinct union of markets (ISO country codes) drawn from this item's active shipping configurations. An empty array means the item has no active shipping configuration and should be hidden from all locale-filtered catalogues (fail-closed). Always serialised. - `badges` (array, nullable): Short display badges rendered as pills on the product card (e.g. "Free shipping", "No minimums", "Ground only"). Order is preserved. Always serialised as an array — consumers can rely on the field being present (empty array means no badges). - `slug` (string, nullable): URL-friendly slug for the stock item, used to form canonical product URLs in consuming storefronts (e.g. /products/{slug}). Lowercase, alphanumerics and hyphens only, globally unique across active non-deleted stock items. - `seoTitle` (string, nullable): Optional override for the HTML tag on the storefront product page. Falls back to the default title if null or empty. - `seoDescription` (string, nullable): Optional override for the HTML meta description on the storefront product page. Falls back to the default description if null or empty. - `priceBands` (array, nullable): Quantity-break pricing for this stocked item, at your account's tier. #### Schema: `StockVariantDto` A Colour variant of a stock item - `variantCode` (string, nullable): The unique identifier for the Stock Variant - `colour` (string, nullable): The Stock Variant Colour - `variantImageUrl` (string, nullable): Gets or sets the URL of the image associated with the product variant. - `numberOfSizes` (integer): The number of sizes available for this variant - `sizesCsv` (string, nullable): Gets or sets a comma-separated list of sizes. #### Schema: `StockImageDto` CW-4346: a single image in a stock item's ordered gallery. Index 0 in Codewolf.Ctrl.Common.Classes.Stocks.StockDto.Images is the hero (the storefront hero CDN URL, `StockDto.StockImageUrl`, mirrors that entry's Codewolf.Ctrl.Common.Classes.Stocks.StockImageDto.Url). - `url` (string, nullable): CDN URL of the image. Use this directly for both rendering and linking. - `name` (string, nullable): Optional display name for captions / lightbox titles. May be null. - `altText` (string, nullable): Optional alt text for accessibility. May be null. Consumers should fall back to a sensible default (e.g. the stock name) when absent. #### Schema: `StockItemShippingSummary` Lightweight shipping configuration summary for inclusion in StockDto responses. Contains only the fields needed for the GET /stock/{code} enrichment. - `id` (integer): Identifier for this shipping configuration row. - `markets` (array, nullable): Market codes this configuration applies in. - `shipMode` (string, nullable): How the item ships — bundled with the order, or as its own consignment. - `weight` (number, nullable): Shipping weight, in `weightUnit`. - `weightUnit` (string, nullable): Unit for `weight` — `kg` or `lb`. - `length` (number, nullable): Package length, in `dimensionUnit`. - `width` (number, nullable): Package width, in `dimensionUnit`. - `height` (number, nullable): Package height, in `dimensionUnit`. - `dimensionUnit` (string, nullable): Unit for `length`, `width` and `height` — `cm` or `in`. - `fixedShipping` (number, nullable): A flat shipping charge replacing live rating. Null means rate normally. - `groundOnly` (boolean): True when the item cannot travel by air. - `estimatedLeadDays` (integer, nullable): Working days to expect before despatch. - `fulfilmentLocations` (array, nullable): The warehouses this item can ship from. - `shipmentTypeOptions` (array, nullable): Per-shipment-method overrides for this stock item's shipping entry. Each option's `price` is null/absent when the rate-API price should be used, `0` for a free override on this method, or positive for a flat override amount. #### Schema: `StockItemShippingLocationSummary` Lightweight fulfilment location summary for StockItemShippingSummary. - `locationCode` (string, nullable): Short code for the fulfilment location. - `locationName` (string, nullable): Human-readable name of the fulfilment location. - `locale` (string, nullable): The region the location sits in. #### Schema: `StockItemShipmentTypeOptionSummary` Lightweight shipment-type option summary for StockItemShippingSummary. Mirrors the API-layer DTO shape: per-method override of shipment type availability and price. - `code` (string, nullable): Shipment type code, as sent on a job. - `name` (string, nullable): Short display abbreviation for the shipment type. - `price` (number, nullable): Charge for this shipment type, in the account's currency. ### GET /Stock/{code} **operationId:** `getStockByCode` **Summary:** Get a List of stock and its variants 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`. **Parameters** - `code` _(required)_ — in `path`: A stock Code or stock variant code - `includePricing` — in `query`: - `variantSortColumn` — in `query`: - `variantSortDirection` — in `query`: - `includeArchived` — in `query`: No description. **Responses** - **200** — Success _One stocked item, with variants and shipping configuration_ ```json { "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 } ] } ``` - **400** — Bad Request — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. ## 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 **operationId:** `createJobs` **Summary:** Create a job 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. **Parameters** - `X-Application-Name` — in `header`: An Valid OriginCode value (this will be validated) _Dry run — garments you sent in, decorated. This is the WHOLE payload, artwork included: to create the job, send these same bytes with validateOnly false, and the response example is what comes back. ⛔ `externalArtworkUrl` must be an absolute https URL on an origin approved for your account (otherwise error 10408 or 10403), and if any PriceCode line carries it they all must (10404). (Ordering a made-to-order garment is a different shape — see the BOM section.)_ ```json { "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" } } ``` **Responses** - **201** — Created _Job created — the same payload with validateOnly dropped. The decoration line was filed under a new asset tag_ ```json { "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 } ] } ``` - **400** — Bad Request — Validation rejected the job and nothing was created. The body carries `validationMessages` with an `errorCode` and a human-readable `message` per problem — for example an asset that is not in an orderable state. - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **500** — ⚠️ On this endpoint a 500 usually means an INVALID PAYLOAD, not an outage. Known triggers: an empty `items` array; a delivery address that cannot be routed; an asset `code` that does not exist; a missing `dateDue`; a `sizeQuantities` key that is not one of the variant’s sizes. Send the same payload with `"validateOnly": true` and fix what it reports before retrying. #### Schema: `JobCandidate` Job class to enable the creation of jobs from the web API - `orderNumber` (string, nullable): (optional) Order # reference (for the creating user's companies Order #) - `orderComment` (string, nullable): Any comment associated with the Order - `customerUserId` (integer, nullable): (Optional) The associated Customer User Id - defaults to current logged on user. When supplied, the user must exist, be active, and belong to the customer the job is created for, and is set as the job's Customer User (owner/contact). - `dateDue` (string, nullable): The ship date you are asking for. ⚠️ OPTIONAL, measured 2026-08-29: omitting it validates, as do `null` and omitting it alongside `mustDate: true`. A date in the PAST is rejected with error `10303` ("Due date must be after today") rather than silently moved, so check it against `GET /Jobs/earliest-ship-date` and take the date from `shippingDateLocal`. An unparseable string is rejected by model binding with an `errors` object instead of `validationMessages`, so handle both shapes. ⛔ Its absence is NOT a cause of an opaque 500 on this endpoint — that claim stood here until 2026-08-29 and was a misattribution; see `JobLineBom.size`. - `mustDate` (boolean): only applicable if due date is specified. Indicates the Due date is a Must be met date - `timeSensitive` (boolean): Indicates whether this job is Time Sensitive which means that the DueDate will incorporate the default Due TIME of the day for the Due date - `description` (string, nullable): A description of the order - `deliveryAddress` (object, nullable): Delivery address information - `promocode` (string, nullable): (Optional) Promo code - Must be a valid Promo code or Job will return a validation error - `items` (array, nullable): A List of Items to add to the Job. Supports adding the inherited classes JobLineAsset, JobLinePriceCode, JobLineStock, JobLineBom Items will be added in the same order as this supplied list - `orderGroup` (string, nullable): Shared identifier linking multiple jobs from one checkout (e.g. RO-1234567). Null for single-job orders. - `orderGroupSequence` (integer, nullable): Position within the order group (1, 2, 3...). Null for single-job orders. - `validateOnly` (boolean): Run the whole authentication and validation path and create nothing, answering `"Validated with NO Errors. Job NOT created."` when the payload is good. Send the payload you actually intend to create — `externalArtworkUrl` included. Measured 2026-09-01: the dry run checks each artwork URL's shape and origin and neither fetches nor stores it, so a validated payload and a created one differ by this flag alone. - `locationCodeOverride` (string, nullable): (Optional) Override the customer's default factory location. When provided, the job is routed to this location instead of the customer's profile location. Must reference an active, non-virtual Location.Code (e.g., "LA", "AT", "NZ"). When omitted or null, existing behaviour is unchanged. #### Schema: `JobLineBaseGrouping` - `groupHead` (string, nullable): (optional) If this Line Item is to be a Grouping for other lines. Note this can contain any string value, but if specified in the head the children will use the same string value This means that other Line items with this same value set in the Group field will be grouped under this item - `group` (string, nullable): (optional) If this line is set it should match to the name of a Group Head on another line, and this will create the current line as a child line under the Group Head line (of the same group name) #### Schema: `JobLineBase` Abstract class for a basic Job Line - `itemType` (object) _(required)_: Which kind of line this is, and therefore what `code` must contain. `"PriceCode"` — a new decoration, `code` is a full price code from `GET /PriceCodes/price-codes`. `"Asset"` — a repeat, `code` is an asset tag from `GET /Assets`. `"Stock"` — stocked goods, `code` is a `stockCode` from `GET /Stock`. `"Bom"` — a made-up product, `code` is a `bomVariantCode` from `GET /Boms/{bomCode}`. `"ExternalGarment"` — a garment you are sending in, which carries no `code` at all. These values are published as an enum on `JobLineType` and as the `itemType` discriminator on the line union, so a generated client already knows them; this list says what the `code` must be for each. - `code` (string, nullable): Identifies the item, in the form `itemType` selects: a full price-code string such as `DG_DIGICAD_FLEX220:Cad Cut Film-Flex 220` (PriceCode), an asset tag such as `EW49123` (Asset), a stock code such as `5000` (Stock), or a BOM variant code such as `BOM10112-M` (Bom). ⛔ `ExternalGarment` lines carry no code. Passing the wrong form for the type is the most common cause of a rejected job. - `quantity` (integer): How many to make. Ignored for `Stock` lines, which carry their run in `sizeQuantities` instead. Quantity drives which price band applies, so ordering the same design as two lines costs more than one line of the combined quantity. - `customerReference` (string, nullable): Your own reference for this line, echoed back per line so you can match a returned asset tag to the line you sent. ⛔ NOT required, 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 and `""` 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 is unreliable — three colliding lines reported "1 duplicated values" and two reported "2" — and it names neither the value nor the line index. Give every line its own reference and none of this applies. #### Schema: `JobLineBom` Defines a Line of a Job which uses a Bill of Material code to identify an entire process for creating an item - `size` (string, nullable): The size code for this BOM line, from the `bomVariants` of `GET /Boms/{bomCode}`. Required when the BOM code does not already carry a size indicator. ⛔ ALWAYS SEND IT. Measured 2026-08-29: when it is required and ABSENT — or explicitly `null` — the request answers an opaque **500**, while `size: ""` reaches the real validator and returns a structured `1825 "Size is required"`. The crash is the absent case, not the invalid one. A code that already carries its size (`BOM10112-M`) does not need it and does not crash without it. - `customName` (string, nullable): An optional per-item personalisation, such as a back name from an "add a name" option at your checkout. It produces a per-name line rather than one aggregated line. #### Schema: `JobLineExternalGarment` Defines a new Job Line which uses an existing Stock item - `sizeQuantities` (object, nullable): How many of each size are coming in, keyed by size code. The size run and its order come from `GET /Lookups/size-sets`. - `garment` (string, nullable) _(required)_: ⛔ REQUIRED, though the schema does not mark it. What the garment is, in words. Omitting it is error 1900. - `description` (string, nullable) _(required)_: ⛔ REQUIRED, though the schema does not mark it. A garment you are sending in must carry a non-empty description or the job is rejected with error 1901 — verified omitted, empty and grouped-without-one. - `quantity` (integer): The quantity from the array of Size * Quantities list #### Schema: `JobLinePriceCode` defines a Job line which is used with a Price Code in the system to define a new Job Asset - `attributes` (object, nullable): The answers to this price code’s own `attributes` questions, keyed by each attribute’s `name` exactly as given. Read them from `GET /PriceCodes/price-codes` — they differ per code and the keys are case-sensitive. - `externalArtworkUrl` (string, nullable): A URL to the artwork for this new decoration. One request places the order and delivers the art; the URL must stay reachable until the job reaches production, since we fetch it rather than holding your link. Include it in your `validateOnly` dry run — validation checks it and creates nothing. Three rules, each measured 2026-09-01 and each answering by number. (1) An ABSOLUTE `https://` URL: `http://`, a bare host, a relative or protocol-relative path and any other scheme are error `10408`, as is a whitespace-only string. (2) Its ORIGIN must be on your account's approved artwork origins — anything else is error `10403`, and `10409` means no origins are configured on the account at all. Both are account configuration rather than something wrong with your URL, so ask us to approve the origin you serve artwork from. Reachability is NOT checked here: a URL that resolves and one that does not answer identically, so a `10403` is never about the file. (3) ALL-OR-NOTHING across the job — if any `PriceCode` line carries artwork, every `PriceCode` line must, or the job is error `10404` naming the line that does not. `Asset` lines are repeats and are unaffected. ⚠️ Omitting the property, `null` and `""` all skip these checks and validate, which means the artwork was not verified — not that it passed. - `externalId` (string, nullable): (Optional) External identifier from third-party systems (e.g., BuildAGangSheet designId) Used to link the asset back to the original external design for editing - `isSample` (boolean): Gets or sets a value indicating whether this instance is marked as a sample. This allows the job line to be identified as a sample item, and therefore bypass the normal quantity minimums #### Schema: `JobLineStock` Defines a new Job Line which uses an existing Stock item - `variantCode` (string, nullable): a variant code which can also identify the Stock item - `sizeQuantities` (object, nullable): A list of size quantities for a stock item Dictionary indexed via SizeCode - `quantity` (integer): The quantity from the array of Size * Quantities list #### Schema: `JobLineAsset` Create a Job Line Item which uses an existing Asset in the system. Identified via the Asset Code (Code property) - `garment` (string, nullable): (optional) Garment - `comment` (string, nullable): Any Comment #### Schema: `ShopJobCandidate` Create Job Candidate for Shop information Extends the Job Candidate object to include the prices/taxes paid - `itemsTotal` (number): Total value of the Items - `taxTotal` (number): Total amount of the Tax charged - `taxLines` (array, nullable): The TAX lines as divided up and included in the Tax amount - `totalAmount` (number): The total Amount of the Order - `shippingTotal` (number): The total amount of shipping paid - `shippingType` (string, nullable): Type of shipping - `paymentType` (array, nullable): How was the order paid for - may list multiple sources - `discounts` (array, nullable): Any discount codes that contributed to the price - `userDetails` (object, nullable): The Shopify users details (see if we need to create the user at the same time as the Job) (Optional) - only passed if we need to create the customer and user for the Shopify user - `customerId` (integer, nullable): The customer Id passed in if we know the customer/user has already been created - `userId` (integer, nullable): The user Id passed in if we know the customer/user has already been created #### Schema: `CustomerShopJobCandidate` - `shopName` (string, nullable): The Shop Name This is the shop name for the link of the order - `shopOrderName` (string, nullable): The Shopify Order Name - `shopOrderId` (string, nullable): The Shop Order Id This is different from the JobCandidate.OrderNumber This is the BigInt order id for the shop - `currencyCode` (string, nullable): The paid currency #### Schema: `TaxLine` - `title` (string, nullable): The type of tax collected - `rate` (number): the rate for the tax collected - `price` (number): the tax collected for this type #### Schema: `ShopDiscount` represents a discount application - `name` (string, nullable): The name for Discount - may be either the discount title or the dicount code - `target` (object): The target for the discount, shipping or Items - `value` (number): The discount Value - as a money value - `targetValue` (object): the type of value, either a percentage or a fixed value - `isDiscountCode` (boolean): Indicates whether this is a discount code or not #### Schema: `ShopUserDetails` - `shopUserId` (integer): Identifier of the shopper in the originating storefront. - `bypassCustomerMatch` (boolean): indicates whether to bypass the customer match checks this is normally set from the Shopify Admin app once an admin user has decided to create the Customer even though there was a previous customer match #### Schema: `UserDetails` - `email` (string, nullable): Email address for order correspondence about this job. - `firstName` (string, nullable): Given name of the person the job is for. - `lastName` (string, nullable): Family name of the person the job is for. - `name` (string, nullable): Full display name, where supplied instead of the separate name parts. - `phone` (string, nullable): Contact phone number for queries about the job. - `userAddress` (any, nullable): Address associated with the person, where one was supplied. #### Schema: `BaseAddress` - `streetAddress` (string, nullable): Street Address for Delivery/Shipping - `address2` (string, nullable): Street Address (2nd line) for Delivery/Shipping - `suburb` (string, nullable): Suburb (or area) for Delivery/Shipping - `city` (string, nullable): City for Delivery/Shipping - `state` (string, nullable): State (or area) for Delivery/Shipping - `stateCode` (string, nullable): State code i.e. NSW, CA, FL - `postalCode` (string, nullable): Postcode for Delivery/Shipping - `countryCodeISO2` (string, nullable): Two-character ISO 3166-1 alpha-2 country code — `"US"`, `"NZ"`, `"GB"`. ⚠️ Two letters, not three: `"USA"` is rejected. Valid codes come from `GET /Lookups/countries` (`iso2`). - `country` (string, nullable): Country Name #### Schema: `DeliveryBaseAddress` Simplified address class for use in api for entering initial job delivery address. - `deliveryMethod` (string, nullable): Delivery method for the order shipping This must be one of the valid delivery methods available. see Http GET: delivery methods - `shippingInstructions` (string, nullable): Any shipping instructions for the delivery - `contactName` (string, nullable): Contact Name for Delivery/Shipping - `organisation` (string, nullable): Organisation for Delivery/Shipping - `phone` (string, nullable): Phone for Delivery/Shipping - `mobile` (string, nullable): Mobile for Delivery/Shipping - `emailAddress` (string, nullable): Email Address for Delivery/Shipping - `isSaturdayDelivery` (boolean): Indicates whether Saturday delivery is requested for this shipment. Supported by UPS and FedEx express services. Optional, defaults to false. #### Schema: `ShippingAddress` One shipment on a job as returned by the job read endpoints — an address plus its delivery method and tracking details. - `shipmentId` (integer): Stable identifier for this shipment on the job (dbo.JobShipment.Id). A job can carry several shipments, each with its own address and delivery method; this is what PATCH /Jobs/{jobNumber}/shipping uses to target one of them. - `trackingLink` (string, nullable): A Tracking link - if one has been allocated - `trackingNumber` (string, nullable): The Tracking number associated to the Shipping address (for the service) #### Schema: `CreateJobReturnDto` Returns the information from a successful Job Creation to the caller - `jobNumber` (integer): The new Job number created for the Job - `location` (string, nullable): indicates the location that the job will be manufactured - `dateDue` (string, nullable): The DueDate. Either the passed DueDate or Calculated due date if requested was earlier than possible NOTE: that this will always be the date and/or time in the locations timezone - `jobLineDetails` (array, nullable): A list of unique key values passed in as part of the CreateJob Item - these should only include job items that require new artwork to be uploaded - `totalJobCost` (number): Total Job Cost - `expectingArtworkToBeUploaded` (boolean): Indicates to the WebApi User whether this newly created Job is expecting Artwork for any of the lines in the Job Itself #### Schema: `JobLineReturnDto` Base class returned as an array of items - holds the return information for each Job line NOTE: Create either a JobItemPriceCodeReturnDto object or one of these which doesn't extend the basic data in here - `needsArtworkToBeUploaded` (boolean): indicates whether artwork NEEDS to be uploaded for this item - `customerReference` (string, nullable): The customer reference Id value that is passed on new PriceCode lines and passed back when new Artwork is needed to be uploaded for the line - `quantity` (integer): the unit cost of the job line (priced based on customer price tier etc.) #### Schema: `JobLinePriceCodeReturnDto` The Job line information returned to the WEBAPI caller from the Create Job call - `newAssetSku` (string, nullable): The associated AssetSKU associated with the job line Shows the new AssetTag code if the JobLine used a Price Code - `jobLineLabelUrl` (string, nullable): If this is an external supplier order, this will contain the link to the jobline label #### Schema: `CreateShopJobReturnDto` Returns the information from a successful Job Creation to the caller - `customerId` (integer): The Customer Id for this shopify user - `userId` (integer): The User Id for this shopify user ### GET /Jobs/{jobNumber} **operationId:** `getJobsByJobNumber` **Summary:** Get the full Job information 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. **Parameters** - `jobNumber` _(required)_ — in `path`: A Job Number **Responses** - **200** — Success _Full job detail — status, money, lines and tracking_ ```json { "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 } ] } ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **404** — Not Found — No job with that number on your account. `GET /Jobs/active` lists everything currently open. #### Schema: `JobDto` The Job record - `stockStatus` (string, nullable): The plain-English stock status for the job: "No Stock", "Partial Stock" or "Stock Complete". Tenant-conditional: only populated for tenants with the ShowInwardsList org pref enabled, and omitted entirely from the response otherwise. Only meaningful where inwards is in use. - `customerUserId` (integer, nullable): The Customer User (owner/contact) the job belongs to - tblJobs.OwnerID. This is the customer's own user who placed the job, not Codewolf.Ctrl.Common.Classes.Jobs.JobBaseDto.Creator (the Control staff member who keyed it in) and not the owning customer. Null on legacy rows that were written without an owner. - `customerUserName` (string, nullable): The display name of the Customer User identified by Codewolf.Ctrl.Common.Classes.Jobs.JobDto.CustomerUserId. Returned alongside the id because a plain CustomerUser token cannot resolve ids itself (/account/users is admin-only), so an id on its own would render as a bare number for exactly the users this field exists to serve. Null when the job has no owner. - `shippingAddresses` (array, nullable): Holds the Shipping addresses and any tracking links - `lines` (array, nullable): The Lines in the Job - `location` (object, nullable): Location information for the job - `taxTotal` (number, nullable): The total tax amount for all job lines - `dateOut` (string, nullable): The date the job was shipped/dispatched - `dateIn` (string, nullable): The date the job was created/entered - `shippedDateStatus` (string, nullable): A formatted status string indicating if the job was shipped early, on time, or late (e.g., "1.35 Days Early", "On Time", "2 Days Late") - `shippedDaysToProcess` (number, nullable): The number of days the job was shipped early or late relative to the due date. Negative values indicate early shipment, positive values indicate late shipment. - `processingDays` (number, nullable): The number of days it took to process the job from creation to shipping - `permissions` (object, nullable): Edit permissions for this job - `isCancelable` (boolean): Indicates whether the job can be cancelled via the API. This is determined by the CanCancelCustomer flag on the job's current MasterJobStatus. - `invoiceFile` (object, nullable): The downloadable invoice file for this job. Populated only when the job is Dispatched or Closed AND an invoice PDF has been stored for it (Control generates and stores invoices; the API never generates them). Null otherwise. The URI points at Control's public, time-limited CDN invoice endpoint. - `customerId` (integer): Customer ID that owns this job. Hidden from JSON output (matches the `OwnerId` pattern on Codewolf.Ctrl.Common.Classes.Jobs.JobExtendedDto) — used at the controller layer for a strict customer-isolation re-check after the legacy `(CustomerMismatch AND UserMismatch)` ownership check, to defend against `OwnerId` collisions on retail rows. #### Schema: `JobBaseDto` The Job record - `jobNumber` (integer): Job Number which identifies the job - `description` (string, nullable): The description associated with the Job - `comments` (string, nullable): The comments associated with the Job - `jobStatus` (string, nullable): The current status of the Job - `orderNumber` (string, nullable): Any associated Order Number information for the Job - `dateDue` (string): The Date the Job is due to complete - `mustDate` (boolean): Indicates if the Due Date is a Must be on time flag - `creator` (string, nullable): Person who Created the Job - `invoiceNumber` (string, nullable): Invoice Number (if been invoiced) - `orderGroup` (string, nullable): Shared identifier linking multiple jobs from one checkout - `orderGroupSequence` (integer, nullable): Position within the order group (1, 2, 3...) #### Schema: `JobLineDto` defines a Line of a Job - `jobLineId` (integer): Unique Identifier for the Job Line - `assetSku` (string, nullable): The Program value assigned to the Job Line This could be the Asset Code or a new Asset code created from a PriceCode - `processCode` (string, nullable): The production process for this line. Values come from `GET /Lookups/process-codes`. - `garment` (string, nullable): The garment to be used - `description` (string, nullable): The description o the Job line - `comments` (string, nullable): The Comments on the Job line - `quantity` (integer): The quantity of this Job Line - `unitPrice` (number): The price of each unit - `customerReference` (string, nullable): The Customers reference string - `imageUrl` (string, nullable): CDN URL for the jobline image (asset or stock variant thumbnail) - `gang` (string, nullable): Gets or sets the name of the gang associated with the entity. - `jobLineStatus` (string, nullable): The line's current production status. ⚠️ Values come from `GET /Lookups/job-statuses`; do not hardcode them. (Upstream's own description of this field — "the programmatic comment associated with this instance" — is a copy-paste error and describes a comment field, not this one.) - `parentJobLineId` (integer, nullable): The JobLineId of the line this line is grouped under, or null when this line is itself a top-level (head) line. Lines sharing a ParentJobLineId belong to the same decoration - for example an applique head line with its outer stitch and repeat setup. Grouping can nest more than one level deep (a stock garment, a decoration on it, and that decoration's setup line). When not null the value always refers to another line present in the same response, so it is safe to use as a key when building a tree. - `order` (integer): The position of this line among its siblings. It is scoped to the parent, not to the job: a child line's Order restarts at 1 within each parent, and most lines report 0 because a value is only assigned when a job is explicitly reordered. It is therefore not a job-wide sequence and not a usable sort key on its own - group using ParentJobLineId rather than this field. For display position prefer Rank, which is a job-wide sequence that reproduces the Control job screen - but only on the reads that populate it. See Rank for which those are. - `rank` (integer): The line's position on the Control job screen, banded so one number carries both the sequence and the grouping. Where it is populated, sorting by `rank` ascending reproduces Control's display order exactly — no secondary sort, and the lines already arrive in that order. **Not every read populates it.** `rank` is set by `GET /Jobs/{jobNumber}` and `GET /Jobs/{jobIdentifier}/extended`. Reads that do not — the agent job read is the example — leave it at its default of 0 on every line. A response whose lines are ALL 0 is unranked, not same-ranked: fall back to the order the lines were returned in. `rank / 100` identifies the group: lines sharing a hundreds band render together, and the first line of a band is the band's head. A top-level line with no children of its own occupies a band by itself, so a band of one is a standalone line rather than a group — use the band's line COUNT to decide whether to render it as a group. `rank >= 100000` means the line belongs to no group and renders at the bottom, in rank order. ⚠️ It does NOT mean the line is a setup, discount or shipping line, and you cannot use it to identify them: measured over 572 live jobs, 35% have every line in that band (decorations included), 23% have a genuine work line there, and 10% carry a discount or shipping line BELOW it. Treat the band as "Control has not been asked to sort this", never as a line type — hiding or grey-ing it blanks a third of jobs. `rank` and `parentJobLineId` answer different questions and can legitimately disagree: rank is where Control draws the line, `parentJobLineId` is which line it was created from. A setup such as `RESPA` points into its group and still ranks 100000+, because Control renders setups at the bottom. Use `rank` for placement and `parentJobLineId` for lineage. Stable for a given job state, not permanent — editing or reordering a job re-ranks it — and scoped to the job, so ranks are only comparable within one job. #### Schema: `JobLocationDto` Location information for a job - `name` (string, nullable): The name of the location - `timezone` (string, nullable): The timezone of the location (Time zone identifier in IANA TZ database format) #### Schema: `PermissionsDto` DTO for edit permissions information - `canEdit` (boolean): Whether `PATCH /Jobs/{jobNumber}` will be accepted for this job. Read it before amending rather than catching the rejection: a job locks once it is dispatched, closed, returned or cancelled. - `lockedReason` (string, nullable): Why editing is locked, when `canEdit` is false. Null while the job is still editable. #### Schema: `InvoiceFileDto` A slim public representation of a job's invoice file on the Job contract. Only exposes the URI to fetch the invoice and an optional expiry; the full internal EntityFileLite is never exposed on the public job contract. - `uri` (string, nullable): The URI to fetch the invoice PDF. This points at Control's public `/cdn/invoices/{token}` endpoint, where the token is a Hashids code (obfuscation, not encryption) keyed by the tenant Salt, carrying the customer, job and a lease expiry. Control re-validates the lease and job ownership server-side on every request. The invoice is served from storage as-is (Control generates and stores it) — nothing is generated on access. - `uriExpires` (string, nullable): Expiry of the Codewolf.Ctrl.Common.Classes.Jobs.InvoiceFileDto.Uri — the end of the link's lease window. The CDN endpoint rejects the link once this passes. Populated whenever an invoice is advertised. ### PATCH /Jobs/{jobNumber} **operationId:** `updateJobsByJobNumber` **Summary:** Patch job details 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. **Parameters** - `jobNumber` _(required)_ — in `path`: The job number to update _Amend the PO number and push the ship date out — omitted fields are left alone_ ```json { "orderNumber": "PO-10482-REV2", "dateDue": "2026-08-21", "comments": "Customer asked to hold for the revised crest." } ``` **Responses** - **200** — Job details updated successfully _The updated job — the same shape GET /Jobs/{jobNumber} returns_ ```json { "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 } ] } ``` - **400** — Invalid location code (does not exist, inactive, or virtual) — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. - **401** — Unauthorized — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **403** — Job is locked for editing, or a non-SuperUser supplied locationCode (see response body for reason) — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **404** — Job not found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. #### Schema: `PatchJobDto` DTO for patching job details - `description` (string, nullable): The job description shown on the job and on your invoice. - `comments` (string, nullable): Notes carried on the job. Sent to the factory with the order. - `orderNumber` (string, nullable): Your own PO or order reference for this job. - `mustDate` (boolean, nullable): Whether `dateDue` is a hard requirement rather than a preference. Setting it commits the factory to the date, so only use it when the date genuinely cannot move. - `dateDue` (string, nullable): The ship date you are asking for. Check it against `GET /Jobs/earliest-ship-date` — a date the factory cannot meet is rejected rather than silently moved. - `locationCode` (string, nullable): ⚠️ Not available to customer credentials — this field is SuperUser-only. Production location is set by us. Omit it. ### POST /Jobs/{jobNumber}/cancel **operationId:** `createJobsByJobNumberCancel` **Summary:** Cancel a job. Cancelling an already-cancelled job returns success. ⚠️ 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. **Parameters** - `jobNumber` _(required)_ — in `path`: The job number to cancel **Responses** - **204** — Job cancelled successfully (or was already cancelled) - **404** — Job not found or user not authorized to view this job — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. - **409** — Job cannot be cancelled in its current status ### GET /Jobs/active **operationId:** `getJobsActive` **Summary:** Get active jobs for the current customer (by default excludes Closed and Cancelled jobs) 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. **Parameters** - `excludeJobNumber` — in `query`: Optional job number to exclude from results - `page` — in `query`: Page number (1-based) - `pageSize` — in `query`: Page size (default 20, max 100) - `searchText` — in `query`: Search by job number, order number, or description - `includeClosedJobs` — in `query`: Include closed jobs in results. Cancelled jobs are never included - `sortColumn` — in `query`: - `sortDirection` — in `query`: - `orderGroup` — in `query`: Optional order group to restrict results to - `customerUserId` — in `query`: 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. **Responses** - **200** — Success _Every open job for the account in one call_ ```json { "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 } ``` - **400** — customerUserId is not a user of the calling customer — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. #### Schema: `PagedActiveJobsDto` Represents a paginated response containing active jobs. - `items` (array, nullable): The collection of active jobs for the current page. - `totalCount` (integer): The total number of active jobs across all pages. - `pageNumber` (integer): The current page number (1-based). - `pageSize` (integer): The number of items per page. - `totalPages` (integer): The total number of pages available. - `hasPreviousPage` (boolean): Indicates whether there is a previous page available. - `hasNextPage` (boolean): Indicates whether there is a next page available. #### Schema: `ActiveJobDto` Represents an active job with essential tracking and status information. - `permissions` (object, nullable): Edit permissions for this job based on its status. - `jobNumber` (integer): The unique identifier for the job. - `masterJobStatus` (string, nullable): The job's current status. ⚠️ Read the full set from `GET /Lookups/job-statuses` rather than matching strings you have seen before — statuses are added over time, and an unrecognised one must never break your integration. - `stockStatus` (string, nullable): The plain-English stock status for the job: "No Stock", "Partial Stock" or "Stock Complete". Tenant-conditional: only populated for tenants with the ShowInwardsList org pref enabled, and omitted entirely from the response otherwise. Only meaningful where inwards is in use. - `originCode` (string, nullable): The origin code indicating where the job originated from. - `description` (string, nullable): A brief description of the job. - `customerUserId` (integer, nullable): The Customer User (owner/contact) the job belongs to - tblJobs.OwnerID. This is the customer's own user who placed the job, not the Control staff member who keyed it in (that is Creator on the job detail DTO) and not the owning customer. Null on legacy rows that were written without an owner. - `customerUserName` (string, nullable): The display name of the Customer User identified by Codewolf.Ctrl.Common.Classes.Jobs.ReturnDtos.ActiveJobDto.CustomerUserId. Returned alongside the id because a plain CustomerUser token cannot resolve ids itself (/account/users is admin-only), so an id on its own would render as a bare number for exactly the users this field exists to serve. Null when the job has no owner. - `mustDate` (boolean): Indicates whether the job has a strict deadline that must be met. - `dateDue` (string, nullable): The date the job is due to be completed. - `dateOut` (string, nullable): The date the job was shipped or dispatched. - `shippedDaysToProcess` (number, nullable): The number of days the job was shipped early or late relative to the due date. Negative values indicate early shipment, positive values indicate late shipment. - `orderNumber` (string, nullable): Order number reference - `orderGroup` (string, nullable): Shared identifier linking multiple jobs from one checkout (e.g. RO-1234567). Null for single-job orders. - `orderGroupSequence` (integer, nullable): Position within the order group (1, 2, 3...). Null for single-job orders. - `tracking` (array, nullable): Tracking information for this job's shipments. Each entry represents one shipment with its tracking number and optional tracking link. Empty list if no tracking data is available. #### Schema: `TrackingItemDto` Represents a single tracking entry for a job shipment. Contains the tracking number and optional tracking link URL. - `trackingNumber` (string, nullable): The carrier tracking number for this shipment. - `trackingLink` (string, nullable): The URL to track this shipment on the carrier's website. May be null if the carrier does not provide a tracking link. ### GET /Jobs/delivery-options **operationId:** `getJobsDeliveryoptions` **Summary:** Get delivery options available to customer 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. **Responses** - **200** — Delivery methods available to this account _Delivery methods available to this account_ ```json [ { "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 } ] ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. #### Schema: `IDeliveryOption` - `code` (string, nullable): The delivery method code. This is the value to send as `deliveryAddress.deliveryMethod` on `POST /Jobs`. - `label` (string, nullable): Short display abbreviation, e.g. `GND`. For your UI; do not send it back. - `isCollection` (boolean): True when the customer collects from the factory rather than the order being shipped. A collection method needs no courier address. - `rank` (integer): Display order for a normal job. Lower sorts first. - `gangRank` (integer): Display order when the job is a gang sheet, which can differ from `rank`. - `value` (string, nullable): The same value as `code`, provided for form bindings. - `deliveryLabelEnabled` (boolean): Whether a delivery label can be produced for this method. ### GET /Jobs/earliest-ship-date **operationId:** `getJobsEarliestshipdate` **Summary:** Get calculated shipping date based on process codes 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. **Parameters** - `processCodes` — in `query`: Optional comma-separated process codes (e.g., "PR,WE") **Responses** - **200** — Shipping date calculated successfully _Soonest despatch for the processes being quoted_ ```json { "shippingDateUtc": "2026-08-05T18:00:00+00:00", "shippingDateLocal": "2026-08-06T06:00:00+12:00", "timezone": "Pacific/Auckland", "dateExclusions": [] } ``` - **401** — User is not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **404** — User not found or no shipping date available — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. #### Schema: `ShippingDateResponseDto` Response containing calculated shipping date and date exclusions for calendar blocking - `shippingDateUtc` (string): Calculated earliest shipping date in UTC - `shippingDateLocal` (string): Calculated earliest shipping date in customer's local timezone - `timezone` (string, nullable): IANA timezone used for calculations (e.g., "Pacific/Auckland") - `dateExclusions` (array, nullable): List of date exclusions (holidays, non-working days) to block on calendar UI #### Schema: `ShippingDateExclusionDto` Date exclusion (holiday/non-working day) for calendar blocking - `dateFrom` (string): Start date of the exclusion period (date only, no time) - `dateTo` (string): End date of the exclusion period (date only, no time) ## 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 **operationId:** `getInwards` **Summary:** List inwards records for the authenticated customer, with optional filters. 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. **Parameters** - `allocated` — in `query`: false = unallocated (job=0), true = allocated (job>0) - `dateFrom` — in `query`: Filter by delivery date from (ISO 8601) - `dateTo` — in `query`: Filter by delivery date to (ISO 8601) - `status` — in `query`: Filter by status name (e.g. "Arrived") - `jobNumber` — in `query`: 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` — in `query`: Page number (1-based, default 1) - `pageSize` — in `query`: Items per page (default 20, max 100) **Responses** - **200** — Success _Deliveries you have sent in, newest first — one allocated to a job, one not yet_ ```json { "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 } ``` - **401** — Unauthorized — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **500** — Server Error — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `InwardsListResponseDto` Paginated list response for GET /Inwards. - `data` (array, nullable): The deliveries on this page. ⚠️ Nullable — normalise it before iterating. - `page` (integer): Which page this is. One-based. - `pageSize` (integer): How many records one page can hold. - `totalItems` (integer): How many deliveries match across every page. ⚠️ Guard a page past the end BOTH ways, because the shape changed: since CW-4654 it returns the real totals with an empty `data` (measured 2026-08-28: `page=9999` → `totalItems: 3305`, `totalPages: 133`, no rows), and older builds returned `totalItems: 0`, which is indistinguishable from an empty collection. `page > 1 && (totalItems === 0 || (totalPages > 0 && page > totalPages))` catches both; reset to page 1. - `totalPages` (integer): How many pages the result set spans at the current `pageSize`. #### Schema: `InwardsSummaryDto` Summary representation of an inwards record (used in list responses). - `id` (integer): The delivery, for `GET /Inwards/{id}`. Not your order number. - `orderNumber` (string, nullable): The order reference the goods were shipped under — yours, echoed back. This is what you match a delivery to your own purchase order by. - `supplier` (string, nullable): Who the goods came from. Often the garment supplier rather than you. - `dateIn` (string, nullable): When the delivery arrived. Null while it is still expected. - `cartons` (integer, nullable): How many packages arrived, as counted at goods-in. - `packageType` (string, nullable): What they arrived in — carton, satchel and so on. Free text. - `totalQuantity` (integer): Total units across every line of the delivery. - `itemCount` (integer): How many distinct lines the delivery has, not how many units. - `status` (string, nullable): ⚠️ Where the delivery has got to, from `GET /Lookups/inwards-statuses`. In practice this has very little variance — nearly every record reads `Arrived` — so use `isAllocated` to tell a delivery still waiting to be matched from one already on a job. - `isAllocated` (boolean): Whether the delivery has been matched to a job yet. This, not `status`, is the field that moves. - `jobNumber` (integer, nullable): The job the delivery was allocated to, or null while it is unallocated. Filter the list by `?jobNumber=` to get every delivery for one job. - `description` (string, nullable): Auto-generated summary of the line items ("code garment colour" per line, CRLF-separated, capped at 250 characters). Maintained by the same routine the intranet uses, so this is the identical string the legacy inwards list renders. Null or empty on records whose line items have no code/garment/colour — those lines are skipped when the summary is composed. #### Schema: `InwardsDto` Detailed representation of an inwards record including items. - `supplierId` (integer, nullable): The supplier the goods came from, by id. - `dateDue` (string, nullable): When the delivery was expected, which is not when it arrived. - `shelfLocation` (string, nullable): Where the goods are physically held in the factory once booked in. - `comment` (string, nullable): Free-text note recorded against the delivery at goods-in. - `packingSlipNumber` (string, nullable): Packing slip reference for this delivery, or null when none is recorded. Free text, not a number — live values are frequently alphanumeric (for example "ORN0073086") and a placeholder "-" is common, so treat this as an opaque display string. - `scanFile` (object, nullable): Link to the supplier's packing slip as scanned at goods-in, or null when this delivery has no scan on file. Distinct from Codewolf.Ctrl.Api.Dtos.Api.InwardsDto.PackingSlipNumber, which is the reference the supplier printed on the paperwork — this is the document itself. - `items` (array, nullable): The lines of the delivery — each a garment in one colour, counted per size. ⚠️ Nullable: treat an absent array as an empty one rather than letting it reach `.map`. #### Schema: `InwardsScanFileDto` A slim public representation of an inwards scan on the Inwards contract. Mirrors the job contract's invoice file: only the fetch URI and its expiry are exposed, never the internal storage record. - `uri` (string, nullable): The URI to fetch the scan. Points at Control's public `/cdn/inwardscans/{token}` endpoint, where the token is a Hashids code (obfuscation, not encryption) keyed by the tenant Salt, carrying the customer, the inwards id and a lease expiry. Control re-validates the lease and the customer's ownership of the delivery server-side on every request. <br> Requires no Authorization header, so it can be opened or downloaded directly by a browser. - `uriExpires` (string, nullable): Expiry of the Codewolf.Ctrl.Api.Dtos.Api.InwardsScanFileDto.Uri — the end of the link's lease window. The CDN endpoint rejects the link once this passes; re-read the record to obtain a fresh one. #### Schema: `InwardsItemDto` A line item on an inwards record. - `id` (integer): The delivery line. - `code` (string, nullable): The garment code as you supplied it — commonly your own SKU. - `garment` (string, nullable): What the garment is, in words. - `colour` (string, nullable): The garment colour, as counted in. One colour per line. - `sizeSetId` (integer): The size set this line is counted against, by id. - `sizeSet` (string, nullable): The name of the size run — look it up in `GET /Lookups/size-sets` to get the sizes IN ORDER. Sorting them yourself puts `10` before `2` and `XL` before `XS`. - `quantities` (array, nullable): ⚠️ Only the NON-ZERO sizes, and not necessarily in size order. Build the row from the size set and fill it from here, rather than reading this as the whole run. - `total` (integer): Units on this line, across every size. #### Schema: `SizeQuantityDto` A quantity for a specific size label within an item. - `size` (string, nullable): The size code, which is a member of the line's size set. `"Qty"` means one-size-fits-all. - `qty` (integer): How many of that size. #### Schema: `PatchInwardsResponseDto` Response for PATCH /Inwards/{id} — the updated record plus the affected job's recomputed stock status, so a client refreshing a job's stock panel does not need a second call to update the status badge and cannot render a value that disagrees with the job's real state. - `stockStatus` (string, nullable): The current stock status of the job this delivery is now associated with: the job just allocated to, or — when the allocation was cleared — the job it was removed from. One of "No Stock", "Partial Stock" or "Stock Complete", matching the value the job endpoints report for the same job. ### GET /Inwards/{id} **operationId:** `getInwardsById` **Summary:** Get a single inwards record (header + items) for the authenticated customer. 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. **Parameters** - `id` _(required)_ — in `path`: The delivery id, as returned by `GET /Inwards`. Not your order number. **Responses** - **200** — Success _One delivery, item by item, counted per size_ ```json { "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 } ] } ``` - **401** — Unauthorized — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **404** — Not Found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. - **500** — Server Error — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. ## 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 **operationId:** `getAssets` **Summary:** Gets all of the 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. **Parameters** - `page` — in `query`: The page number - `pageSize` — in `query`: The size of the Page in rows - `sortColumn` — in `query`: Column to sort by - `sortDirection` — in `query`: Sort direction (ascending or descending) - `includeProcesses` — in `query`: (optional)If passed, can contain a list of Process codes to include (comma separated) - `excludeProcesses` — in `query`: (optional)If passed, can contain a list of Process codes to exclude (comma separated) - `filter` — in `query`: (optional)If passed will filter the search using the filter text - `isArchived` — in `query`: (optional)If true, returns only archived assets. If false, only non-archived. If omitted, returns both. - `priceCodeContains` — in `query`: (optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains ANY of them. NULL PriceCodes are excluded. - `priceCodeNotContains` — in `query`: (optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains NONE of them, including assets with NULL PriceCode. **Responses** - **200** — Success _Prints already made for this account — reorder any of them by asset tag_ ```json { "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 } ] } ] } ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. #### Schema: `AssetListDto` - `pageSize` (integer): How many records one page can hold — the size requested, not the number returned. - `returnedResults` (integer): How many records this page actually contains. Lower than `pageSize` on the last page. - `totalResults` (integer): How many records match across every page. Use this for "N results", not the page length. - `totalPages` (integer): How many pages the full result set spans at the current `pageSize`. - `currentPage` (integer): Which page this is. One-based — the first page is 1, not 0. - `hasNext` (boolean): Whether another page follows. Prefer this over comparing page numbers yourself. - `hasPrevious` (boolean): Whether a page precedes this one. - `filter` (string, nullable): The filter applied to produce this result set, echoed back. Null when unfiltered. - `nextUrl` (string, nullable): Ready-made URL for the next page. Follow it rather than assembling your own — it carries the filter and page size already. Null on the last page. - `previousUrl` (string, nullable): Ready-made URL for the previous page. Null on the first page. - `entities` (array, nullable): The records themselves. Everything else on this object describes the page, not the data. #### Schema: `AssetListDto` Asset DTO for list endpoints - excludes ExternalId per CW-4024 - `assetId` (integer): Internal numeric id. Use `assetTag` when ordering — that is what a job line accepts. - `assetTag` (string, nullable): The asset's code, and the thing you reorder by: send it as a job line `code` with `itemType: "Asset"`. Reordering a tag reproduces the previous run exactly — no artwork upload, no colour re-approval. - `processCode` (string, nullable): The production process this asset is made with. Values come from `GET /Lookups/process-codes`. - `description` (string, nullable): The asset's name as it appears on jobs and invoices. - `garment` (string, nullable): The garment this asset was set up against, where one was recorded. - `priceTierCode` (string, nullable): The pricing tier the `priceBands` below are quoted at — your account's tier. - `setup` (number): One-off charge to create the asset. Already paid on an existing asset, so a reorder does not incur it. - `reset` (number): Charge applied each time the asset is run again. This is the real cost of a reorder, alongside the unit price. - `priceCode` (string, nullable): The price code the asset was originally created from. - `assetUrl` (string, nullable): Link to a preview image of the asset. Null where no preview has been generated. - `createDate` (string): When the asset was first created, UTC. - `isGlobal` (boolean): True for catalogue assets available to every account rather than ones your account created. Global assets are also listed by `GET /Assets/global`. - `isArchived` (boolean): True when the asset has been retired. Archived assets are hidden from the default library listing — pass `isArchived=true` to list them. ⚠️ Archiving does not block reordering: the record still resolves and an order referencing it is accepted. - `priceBands` (array, nullable): Quantity-break pricing for reordering this asset, at your account's tier. - `attributes` (object, nullable): Free-form asset attributes (the AssetAttributes table) as name/value pairs. <br> Populated for SuperUser callers only. NullValueHandling.Ignore is set explicitly at the property level because the MVC response pipeline (ApiConfigurationHelper.ConfigureControllers) does not override Newtonsoft's default of Include — without this attribute a null would serialize as an explicit "attributes": null key and leak the property's existence to non-SuperUser callers. <br> An empty dictionary means "authorised, but this asset has no attributes"; an absent key means "not authorised to see them". ### GET /Assets/{assetCode}/jobs **operationId:** `getAssetsByAssetCodeJobs` **Summary:** Get all jobs associated with a specific asset code for the authenticated customer 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. **Parameters** - `assetCode` _(required)_ — in `path`: The asset tag, as returned by `GET /Assets`. **Responses** - **200** — Successfully retrieved job list _Every job this asset has been run on_ ```json { "assetCode": "EW49123", "jobs": [], "totalCount": 0 } ``` - **400** — Invalid asset code provided — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. - **401** — Unauthorized - customer ID not found in authentication context — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **500** — Internal server error — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `AssetJobsResponseDto` Response DTO for asset jobs endpoint - `assetCode` (string, nullable): The asset code that was queried - `jobs` (array, nullable): List of jobs containing this asset - `totalCount` (integer): Total count of jobs returned #### Schema: `AssetJobDto` Individual job information in asset jobs response - `jobNumber` (integer): Job number - `orderNumber` (string, nullable): Customer's order number - `description` (string, nullable): Job description - `quantity` (integer, nullable): Quantity from the job line matching this asset (not total job qty) - `dateOut` (string, nullable): Date the job was dispatched (null for active jobs not yet shipped) - `masterJobStatus` (string, nullable): Current job status ### GET /Assets/{assetTag} **operationId:** `getAssetsByAssetTag` **Summary:** Gets an individual Assets information 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. **Parameters** - `assetTag` _(required)_ — in `path`: The asset tag, as returned by `GET /Assets` — the identifier of a decoration we have already made for you. **Responses** - **200** — Success _One asset by tag_ ```json { "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 } ] } ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **404** — No asset with that tag on your account. Asset tags are per-account; list yours with `GET /Assets`. #### Schema: `AssetDto` Describes an Asset Dtos - `assetId` (integer): Internal Id for Asset - `assetTag` (string, nullable): The Tag (code) which identifies a unique asset - `processCode` (string, nullable): The Process the Asset is used in - `description` (string, nullable): The Asset Description - `garment` (string, nullable): The Garment the Asset is used on - `priceTierCode` (string, nullable): The price Tier Code - `setup` (number): The cost to setup for this asset job ($) - `reset` (number): The reset cost to for this asset job ($) - `priceCode` (string, nullable): Price code used to create the Asset - `assetUrl` (string, nullable): URL to the Asset image - `createDate` (string): Create Date - `isGlobal` (boolean): Indicates if the Asset is a Global Asset - that anyone can see/use - `externalId` (string, nullable): External identifier from third-party systems (e.g., BuildAGangSheet designId) - `customerId` (integer, nullable): Owning customer id (ClientId on the underlying asset row). Populated when the asset is fetched via the data-reader constructor (e.g. SuperUser flows) so callers can derive the owning customer from the asset itself. Nullable so the parameterless-constructor path yields null rather than a misleading 0. <br> NOTE: null does NOT mean the key is omitted from API responses. The MVC pipeline (ApiConfigurationHelper.ConfigureControllers) does not override Newtonsoft's default NullValueHandling.Include, so this serializes as an explicit "customerId": null. The global NullValueHandling.Ignore in Startup.cs applies only to manual JsonConvert calls. See API-Technical-Debt item 29. - `isArchived` (boolean): Indicates if the asset is archived (hidden from default view) - `priceBands` (array, nullable): Pricing Bands for the Asset - `attributes` (object, nullable): Free-form asset attributes (the AssetAttributes table) as name/value pairs — the same set shown on the Attributes tab of the Asset dialog in Control (Description, Garment, Colors, Size, PriceCode, ...). The set varies by tenant and process, so it is not a fixed schema. <br> Populated for SuperUser callers only. For every other caller it is left null and the key is omitted from the JSON entirely (see M:Codewolf.Ctrl.Common.Classes.AssetDtos.AssetDto.ShouldSerializeAttributes), so a non-SuperUser response is byte-identical to one produced before this property existed. <br> An empty dictionary means "authorised, but this asset has no attributes" — distinct from the key being absent, which means "not authorised to see them". #### Schema: `AssetExtendedDto` Extends the AssetDto to include the CustomerId for SuperUser users who make the call - `customerId` (integer): Customer Id ### GET /Assets/{assetTag}/files **operationId:** `getAssetsByAssetTagFiles` **Summary:** Get all files associated with an asset The artwork files held against an asset, current and superseded. `historicFiles` records what was replaced and when. **Parameters** - `assetTag` _(required)_ — in `path`: The asset tag, as returned by `GET /Assets`. **Responses** - **200** — Success _Files attached to an asset, current and superseded_ ```json { "assetTag": "EW49123", "currentFiles": [], "historicFiles": [] } ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **404** — Not Found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. #### Schema: `AssetFilesResponseDto` Response DTO for retrieving asset files - `assetTag` (string, nullable): The asset tag (also known as asset code) - `currentFiles` (array, nullable): List of current (active) files associated with this asset (ordered by date uploaded descending) - `historicFiles` (array, nullable): List of historic (archived) files associated with this asset (ordered by date uploaded descending) #### Schema: `AssetFileDto` Individual asset file information - `fileId` (string, nullable): Hashed file identifier (matches the hashId in cdnUrl) Can be used to construct CDN URLs or for download requests - `fileName` (string, nullable): Original filename - `fileSize` (integer): File size in bytes - `dateUploaded` (string): Date and time when the file was uploaded - `isImage` (boolean): Indicates whether this file is an image type (jpg, jpeg, png, gif, bmp) For image files, clients can append ?size=100 to cdnUrl for thumbnails - `cdnUrl` (string, nullable): Full CDN URL for accessing the file For images, append ?size=50, ?size=100, or ?size=150 for thumbnails #### Schema: `HistoricAssetFileDto` Historic (archived) asset file information with action details - `action` (string, nullable): Action that caused this file to be archived (e.g., "Replaced", "Archived") ### GET /Assets/global **operationId:** `getAssetsGlobal` **Summary:** Gets all of the Global Assets 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`. **Parameters** - `page` — in `query`: The page number - `pageSize` — in `query`: The size of the Page in rows - `sortColumn` — in `query`: Column to sort by - `sortDirection` — in `query`: Sort direction (ascending or descending) - `includeProcesses` — in `query`: (optional)If passed, can contain a list of Process codes to include (comma separated) - `excludeProcesses` — in `query`: (optional)If passed, can contain a list of Process codes to exclude (comma separated) - `filter` — in `query`: (optional)If passed will filter the search using the filter text - `isArchived` — in `query`: (optional)If true, returns only archived assets. If false, only non-archived. If omitted, returns both. - `priceCodeContains` — in `query`: (optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains ANY of them. NULL PriceCodes are excluded. - `priceCodeNotContains` — in `query`: (optional)Comma-separated case-insensitive substrings. Asset is included if its PriceCode contains NONE of them, including assets with NULL PriceCode. **Responses** - **200** — Success _Catalogue assets available to every account_ ```json { "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 } ] } ] } ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. ### GET /Assets/types **operationId:** `getAssetsTypes` **Summary:** Gets all of the Asset Types Which asset type each process produces. Useful for labelling assets in your own UI without hardcoding a mapping that changes. **Responses** - **200** — Success _Asset type per process code_ ```json [ { "process": "EW", "assetType": "Embroidery" }, { "process": "MG", "assetType": "Direct Screen" }, { "process": "DG", "assetType": "Digital Transfers" } ] ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. #### Schema: `AssetTypeDto` Holds the information about an Asset Type - `process` (string, nullable): The Process Code which normally is included at the front of the AssetCode so you can understand the process that the Asset uses - `assetType` (string, nullable): The description of the Asset Type ## 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 **operationId:** `getBoms` **Summary:** Get the BOMs available to the logged-on user's customer. 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. **Parameters** - `filter` — in `query`: Substring matched against the BOM code or any variant code. - `includeItems` — in `query`: 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` — in `query`: When true, archived (inactive) BOMs are included, along with their inactive variants. Hard-deleted BOMs are never returned. Defaults to false. **Responses** - **200** — The made-up products this account can order (truncated — the real answer is thousands) _The made-up products this account can order (truncated — the real answer is thousands)_ ```json [ { "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 } ] } ] ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. #### Schema: `BomDto` describes a BOM (Bill of Materials) Lookup Item - `id` (integer): BOM Id - `bomCode` (string, nullable): The code which identifies the BOM - `title` (string, nullable): The BOM's title. Sourced from the BOM.Title column (previously BOM.Description before CW-4323). This is the short descriptive name of the BOM used as the Shopify product title. - `description` (string, nullable): Plain-text description of the BOM. Sourced from the BOM.Description column (added in CW-4323). Manually edited on the BOM edit page or auto-filled from the stock garment's StockSpecifications when the first stock item is added. Null if never populated. - `range` (string, nullable): The range the BOM belongs to - `customerSku` (string, nullable): Customer SKU code (if applicable) - `active` (boolean): Whether the BOM is active. An archived (Active = false) BOM is excluded from the reads by default and is returned only when the caller asks for archived rows explicitly. Added in CW-4664 so a consumer can tell an archived BOM from an active one — previously an archived BOM simply vanished from the response with no way to distinguish it from one that never existed. Hard-deleted BOMs (Deleted = 1) are never returned under any flag. - `imageUrl` (string, nullable): Absolute CDN URL for the BOM's image, or null when the BOM has no image uploaded. Example: "https://tenant-domain.example.com/cdn/boms/UjHmCJKgOu". Added in CW-4664. Shares the URL shape and the underlying EntityFile lookup with `ShopBomDto.ImageUrl`. - `items` (array, nullable): The BOM's composition — the garment plus its branding lines. Populated only when the caller requests it (`?includeItems=true`); `null` otherwise, so the default response shape is unchanged for existing consumers and the list endpoint does not fan out to one row per item across thousands of BOMs. Added in CW-4664. - `bomVariants` (array, nullable): The variants of this BOM (normally relates to sizes of the BOM) #### Schema: `BomItemDto` One component of a BOM — either the garment or one of the branding lines applied to it. Returned on `BomDto.Items` when the caller passes `?includeItems=true`. Added in CW-4664 so a consumer can render a BOM as a product rather than as a code and a size list, and so a proof can be generated from the components and their positions. - `id` (integer): The BOM item's id. Stable, and the handle a future write endpoint would use to address a single item for edit or removal. - `assetCode` (string, nullable): The asset or stock variant code for this component. For the garment this is the stock variant code; for a branding line it is the asset code. - `isStockItem` (boolean): True for the garment, false for a branding line. There is at most one active stock item on a BOM, and it is the item that determines the BOM's size run. - `processCode` (string, nullable): The process applied by this component (for example `STOCK` for the garment, or the decoration process for a branding line). Derived from the asset code by the `tr_SetProcessCode` trigger. - `position` (string, nullable): Where the component sits on the garment — for example "Left chest". Null on the garment itself and on any branding line where a position was never recorded. #### Schema: `BomVariantDto` defines the Variant of a BOM (this normally relates to size variants) - `id` (integer): The BOM variant's own identifier. Additive to the read surface: the variant write endpoint addresses a variant by this id, and there is no other way for a caller to obtain it. - `bomVariantCode` (string, nullable): The BOM variant code - relating to size variants - `size` (string, nullable): The BOM Variant Size NOTE: if the Size string is equal to "Qty" then this implies it is one size fits all and the Qty should just be assigned - `customerVariantSku` (string, nullable): Customer Variant SKU code (if applicable) - `active` (boolean): Whether this variant is active. Added in CW-4664. A BOM with no active variants is not orderable, and before this field a consumer could not tell such a BOM apart from one that never had a size run — the variant join filtered inactive rows out silently. Inactive variants are returned only when the caller asks for archived rows explicitly. #### Schema: `BomExtendedDto` Extends the BomDto to include the CustomerId - for when the call comes in from a SuperUser - `customerId` (integer): Customer Id ### GET /Boms/{bomCode} **operationId:** `getBomsByBomCode` **Summary:** Gets an individual BOM's information, by its BOM code or by any of its variant codes. 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. **Parameters** - `bomCode` _(required)_ — in `path`: 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` — in `query`: 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` — in `query`: When true, an archived (inactive) BOM is returned, and its inactive variants are included. Hard-deleted BOMs are never returned. Defaults to false. **Responses** - **200** — Success _One BOM and its size variants — order by bomVariantCode, never by concatenation_ ```json { "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 } ] } ``` - **403** — Forbidden — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **404** — Not Found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. ## Lookups Reference data — countries, states, job statuses, inwards statuses, process codes, size sets. Read these instead of hardcoding values that change. ### GET /Lookups/countries **operationId:** `getLookupsCountries` **Summary:** Get list of available countries Returns a list of countries available for address selection, along with the tenant's default country. **Responses** - **200** — Successfully retrieved countries _Countries, with which address parts each one needs_ ```json { "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" } ``` - **401** — Authentication required — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **500** — An error occurred — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `CountryListResponse` Response containing list of countries with default - `countries` (array, nullable): List of available countries - `defaultCountry` (string, nullable): Default country ISO2 code for this tenant #### Schema: `CountryDto` Country information for lookup - `name` (string, nullable): Full country name - `iso2` (string, nullable): ISO 3166-1 alpha-2 code - `iso3` (string, nullable): ISO 3166-1 alpha-3 code - `hasSuburb` (boolean): Indicates whether this country uses a suburb/district field in addresses (Dependent Locality) - `hasState` (boolean): Indicates whether this country uses a state/province field in addresses (Administrative Area) - `hasPostalCode` (boolean): Indicates whether this country uses a postal/zip code field in addresses ### GET /Lookups/countries/{countryCode}/states **operationId:** `getLookupsCountriesByCountryCodeStates` **Summary:** Get list of states/provinces for a country 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. **Parameters** - `countryCode` _(required)_ — in `path`: ISO 3166-1 alpha-2 country code (e.g., US, NZ, GB) **Responses** - **200** — Successfully retrieved states _States for a country — empty where the country has none_ ```json { "countryCode": "US", "states": [] } ``` - **401** — Authentication required — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **500** — An error occurred — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `StateListResponse` Response containing list of states/provinces for a country - `countryCode` (string, nullable): Country ISO2 code - `states` (array, nullable): List of states/provinces for the country #### Schema: `StateDto` State/province/region information for lookup - `name` (string, nullable): Full state/province name - `abbreviation` (string, nullable): State/province abbreviation ### GET /Lookups/inwards-statuses **operationId:** `getLookupsInwardsstatuses` **Summary:** Get list of available 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. **Responses** - **200** — Successfully retrieved inwards statuses _The status vocabulary for a delivery — every value, including the blank one_ ```json [ { "id": 1, "name": "" }, { "id": 2, "name": "Cart" }, { "id": 3, "name": "Awaiting Arrival" }, { "id": 4, "name": "Arrived" } ] ``` - **401** — Authentication required — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **500** — An error occurred — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `InwardsStatusDto` An inwards status lookup entry. - `id` (integer): The status, by id. - `name` (string, nullable): The status as displayed. ### GET /Lookups/job-statuses **operationId:** `getLookupsJobstatuses` **Summary:** Get list of available process codes 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. **Responses** - **200** — Successfully retrieved process codes _Every job status — read this rather than matching strings_ ```json [ { "id": 1, "name": "Awaiting artwork" }, { "id": 2, "name": "In production" }, { "id": 3, "name": "Shipped" } ] ``` - **401** — Authentication required — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **500** — An error occurred — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `JobStatusDto` Configuration for a single supported job status - `id` (integer): MasterJobStatusID - `name` (string, nullable): Name ### GET /Lookups/process-codes **operationId:** `getLookupsProcesscodes` **Summary:** Get list of available process codes Returns a list of manufacturing process codes available to you, ordered by rank (ascending). **Responses** - **200** — Successfully retrieved process codes _Process codes with their production characteristics_ ```json [ { "processCode": "EW", "description": "Embroidery", "rank": 10, "metadata": { "setupApplies": true, "resetApplies": false, "scheduleable": true, "physicalStockRequired": false, "printable": true } } ] ``` - **401** — Authentication required — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **403** — Access denied - user lacks required role — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **500** — An error occurred — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `ProcessCodeDto` Process code information - `processCode` (string) _(required)_: Process code identifier - `description` (string) _(required)_: Human-readable process description - `rank` (integer): Display order rank (lower values appear first) - `metadata` (object, nullable): Process metadata containing operational flags #### Schema: `ProcessCodeMetadataDto` Process metadata containing operational flags - `setupApplies` (boolean): Indicates if setup charges apply to this process - `resetApplies` (boolean): Indicates if reset charges apply to this process - `scheduleable` (boolean): Indicates if this process can be scheduled in production - `physicalStockRequired` (boolean): Indicates if physical stock is required for this process - `printable` (boolean): Indicates if this process produces printable output ### GET /Lookups/size-sets **operationId:** `getLookupsSizesets` **Summary:** Get list of available size templates for inwards/sizing operations. 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. **Responses** - **200** — Successfully retrieved size sets _Named size runs used by stock and garment lines_ ```json [ { "id": 3, "name": "Adult", "sizes": [ "S", "M", "L", "XL", "2XL" ] } ] ``` - **401** — Authentication required — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **500** — An error occurred — An unexpected server error. Retry once with backoff; if it persists, contact your account manager with the time of the request and the endpoint. #### Schema: `InwardsSizeSetDto` A size template — e.g. "2XS-5XL", "One Size". - `id` (integer): tblSizeMain.ID - `name` (string, nullable): tblSizeMain.szDesc - `sizes` (array, nullable): Active size labels, in position order (sz1..sz20, skipping null/empty). ## Account Your account: address, users, tax certificates, transactions. `GET /account` confirms which account a set of credentials belongs to. ### GET /account **operationId:** `getAccount` **Summary:** Get account profile 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.) **Responses** - **200** — Account profile retrieved successfully _The account these credentials belong to_ ```json { "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": [] } ``` - **401** — User is not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **404** — Account not found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. #### Schema: `AccountProfile` Account profile information for authenticated customer users. Read-only company/organization-level data. Matches what customers can currently see in the secure portal. - `id` (integer): Customer ID - `name` (string, nullable): Customer name (for reference) - `priceTierCode` (string, nullable): Customer's pricing tier code - `currency` (string, nullable): Currency code for transactions - `taxRateType` (string, nullable): Tax rate type (e.g., "GST", "VAT", "Sales Tax") - `taxRate` (number, nullable): Tax rate percentage (null if using TaxJar or tax exempt) - `taxSystemTaxExempt` (boolean): Whether customer is tax exempt - `taxSystemTaxExemptReason` (string, nullable): Reason for tax exemption (if applicable) - `showPayNow` (boolean): Whether to show "Pay Now" option - `creditCardRequired` (boolean): Whether credit card is required for orders - `shipmentCarriersCSV` (string, nullable): Comma-separated list of available shipment carriers - `excludeShipmentsCsv` (string, nullable): Comma-separated list of excluded shipment types - `courierAccountCode` (string, nullable): Customer's courier account code (if shipping on own account) - `courierSiteID` (string, nullable): Courier site ID - `crmId` (string, nullable): External CRM system ID (for reference only) - `accountManagerName` (string, nullable): Account manager full name (FirstName LastName format) - `accountManagerEmail` (string, nullable): Account manager email address - `tierLast12Months` (string, nullable): Customer tier based on last 12 months of activity - `paymentTermDays` (integer, nullable): Gets or sets the number of days allowed for payment after an invoice is issued. - `paymentTermType` (string, nullable): Gets or sets the type of payment terms (e.g., "Net", "Due on Receipt"). - `entityAddress` (object, nullable): Entity (company) street address associated with this account - `taxCertificates` (array, nullable): List of tax exemption certificates for this account - `paymentMode` (string, nullable): Payment mode for this customer (e.g., "credit_card_required", "pay_up_front"). Determines checkout behaviour for retail integrations. - `paymentMethods` (array, nullable): List of saved payment methods for this account #### Schema: `EntityAddressDto` Entity (company) address associated with a customer account. Read from the EntityAddress table where RelatedToEntityType = 'CUSTOMER'. - `id` (integer): Entity address ID - `streetAddress` (string, nullable): Street address (line 1) - `addressLine2` (string, nullable): Address line 2 - `city` (string, nullable): City - `state` (string, nullable): State/province abbreviation (e.g., "CA", "NY") - `stateFull` (string, nullable): Full state/province name (e.g., "California", "New York") - `postalCode` (string, nullable): Postal/ZIP code - `countryCode` (string, nullable): ISO 3166-1 alpha-2 country code (e.g., "US", "NZ") - `countryName` (string, nullable): Full country name - `contactName` (string, nullable): Contact name - `organisation` (string, nullable): Organisation name - `phone` (string, nullable): Phone number - `emailAddress` (string, nullable): Email address - `addressSummaryOneLine` (string, nullable): One-line summary of the full address #### Schema: `TaxCertificate` Full tax exemption certificate details - `id` (integer): Tax certificate ID - `customerId` (integer): Customer ID (always matches authenticated customer) - `name` (string, nullable): Contact name on certificate - `businessName` (string, nullable): Business name on certificate - `exemptionNumber` (string, nullable): Certificate/exemption number - `employerIdentificationNumber` (string, nullable): Employer Identification Number (EIN/Tax ID) - optional - `businessEntityId` (string, nullable): Business entity ID - optional - `licenseExpiryDate` (string, nullable): License expiry date (null if no expiry) - `exemptionType` (string, nullable): Type of exemption (e.g., "Resale", "Manufacturing", "Non-Profit") - `exemptionState` (string, nullable): State where exemption applies (e.g., "CA", "TX") - `fileName` (string, nullable): Uploaded certificate file name (null if no file uploaded) - `hasFile` (boolean): Whether a certificate file has been uploaded - `createdAt` (string): When this certificate was created - `updatedAt` (string): When this certificate was last updated - `entityFileId` (integer): Identifier of the stored certificate file. - `cdnUrl` (string, nullable): Time-limited link to download the certificate. - `fileStatus` (object): Where the certificate has got to in review. - `fileStatusDescription` (string, nullable): The review status in words, suitable for display. #### Schema: `PaymentMethod` Saved payment method (credit card token) information (read-only) - `id` (string): Token ID (GUID) - `gatewayProvider` (string, nullable): Gateway provider name (e.g., "Stripe", "PaymentExpress", "Windcave") - `cardName` (string, nullable): Card type (e.g., "Visa", "Mastercard", "Amex") - `cardNumber` (string, nullable): Masked card number (e.g., "****1234") - `cardExpiry` (string, nullable): Card expiry (MM/YY format) - `cardHolderName` (string, nullable): Cardholder name - `dateCreatedUtc` (string): When card was added - `isDefault` (boolean): Whether this is the default payment method ### GET /account/shipment-settings **operationId:** `getAccountShipmentsettings` **Summary:** Get shipment settings Retrieves shipment configuration for your organization. **Responses** - **200** — Shipment settings retrieved successfully _Carriers and freight rules configured for the account_ ```json { "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 } ] } ``` - **401** — User is not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **404** — Customer config not found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. #### Schema: `ShipmentSettings` Customer shipment settings - company-level shipping configuration. - `availableCarriers` (array, nullable): Carriers available to this customer. If customer has no restrictions, this contains all carrier options. - `carrierOptions` (array, nullable): All carrier options available for selection (carrier names). - `receiverPays` (object, nullable): Third-party billing configuration for customer's own carrier account. - `showCustomerAddressOnLabel` (boolean): Whether to show customer's address as return address on shipping labels. - `handlingFee` (number): Handling fee amount. Note: Automatically set to 0 when receiverPays is enabled. - `applyHandlingFeeInsteadOfFreight` (boolean): If true, handling fee replaces freight charges. Note: Automatically set to true when receiverPays is enabled. - `jobComment` (string, nullable): Default comment that appears on jobs for shipping department. - `availableShipmentTypes` (array, nullable): Shipment type codes available for this customer. Derived from ExcludeShipmentsCsv (inverted logic). Empty array means all shipment types are available. - `shipmentTypeOptions` (array, nullable): All shipment type options available for selection. - `freightRates` (array, nullable): Freight rate rules with cascading defaults (read-only). #### Schema: `ReceiverPaysSettings` Third-party billing (receiver pays) configuration. - `billingAccountNumber` (string, nullable): Customer's carrier account number for third-party billing. - `billingPostalCode` (string, nullable): Billing postal code for the carrier account. - `billingCountryCode` (string, nullable): Billing country code (e.g., "US", "NZ"). #### Schema: `ShipmentType` Shipment type with code and display name. - `code` (string, nullable): Shipment type code. - `name` (string, nullable): Display name for the shipment type. #### Schema: `FreightRateDto` Freight rate rule from the cascaded view: system defaults → tenant defaults → customer overrides. - `carrier` (string, nullable): Carrier code (e.g. "FedEx", "UPS"). Null for the global default row. - `carrierName` (string, nullable): Carrier display name (e.g. "FedEx", "DEFAULT"). - `service` (string, nullable): Service code (e.g. "fedex_2day"). Null when not applicable. - `serviceName` (string, nullable): Service display name (e.g. "FedEx 2Day®"). Null when not applicable. - `package` (string, nullable): Package code (e.g. "YOUR_PACKAGING"). "DEFAULT" for default package. Null when not applicable. - `packageName` (string, nullable): Package display name. Null when not applicable. - `chargeType` (string, nullable): Charge type: "markup_on_order" (percentage of order value) or "fixed_per_package". - `value` (number): Rate value (percentage or fixed amount). - `minimum` (number, nullable): Minimum charge (null if none). - `isDefault` (boolean): True if this rate is inherited from system/tenant defaults (read-only). False if this is a customer-specific override (editable/deletable). ### GET /account/transactions **operationId:** `getAccountTransactions` **Summary:** Get payment transaction history Retrieves a paginated list of payment transactions for the authenticated customer. Includes card details (masked), amounts, and transaction status. **Parameters** - `page` — in `query`: Page number, 1-based (default: 1) - `pageSize` — in `query`: Number of records per page (default: 20) - `sortBy` — in `query`: Column to sort by (default: Id). Valid values: Id, CreatedAt, CompletedAt, AmountSettlement - `sortDirection` — in `query`: Sort direction: ASC or DESC (default: DESC) - `completedOnly` — in `query`: Filter to show only completed transactions (default: false) - `textFilter` — in `query`: Optional text filter to search transaction details **Responses** - **200** — Transaction list retrieved successfully _Invoices and payments, most recent first_ ```json { "pageSize": 25, "returnedResults": 0, "totalResults": 0, "totalPages": 1, "currentPage": 1, "hasNext": false, "hasPrevious": false, "filter": null, "nextUrl": null, "previousUrl": null, "entities": [] } ``` - **401** — User is not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. #### Schema: `PaymentTransaction` - `pageSize` (integer): How many records one page can hold — the size requested, not the number returned. - `returnedResults` (integer): How many records this page actually contains. Lower than `pageSize` on the last page. - `totalResults` (integer): How many records match across every page. Use this for "N results", not the page length. - `totalPages` (integer): How many pages the full result set spans at the current `pageSize`. - `currentPage` (integer): Which page this is. One-based — the first page is 1, not 0. - `hasNext` (boolean): Whether another page follows. Prefer this over comparing page numbers yourself. - `hasPrevious` (boolean): Whether a page precedes this one. - `filter` (string, nullable): The filter applied to produce this result set, echoed back. Null when unfiltered. - `nextUrl` (string, nullable): Ready-made URL for the next page. Follow it rather than assembling your own — it carries the filter and page size already. Null on the last page. - `previousUrl` (string, nullable): Ready-made URL for the previous page. Null on the first page. - `entities` (array, nullable): The records themselves. Everything else on this object describes the page, not the data. #### Schema: `PaymentTransaction` Payment transaction history record (read-only) - `id` (integer): Transaction ID - `gatewayType` (string, nullable): Payment gateway provider (e.g., "Stripe", "PaymentExpress", "Windcave") - `transactionStatus` (string, nullable): Transaction status (e.g., "Completed", "Pending", "Failed", "Cancelled") - `createdAt` (string): When transaction was created - `completedAt` (string, nullable): When transaction completed (null if pending/failed) - `cardName` (string, nullable): Card type (e.g., "Visa", "Mastercard", "Amex") - `cardNumber` (string, nullable): Masked card number (e.g., "****1234") - `cardExpiry` (string, nullable): Card expiry (MM/YY format) - `currencySettlement` (string, nullable): Settlement currency code (e.g., "USD", "NZD", "GBP") - `amountSettlement` (number): Settlement amount - `merchantEntityType` (string, nullable): Entity type this transaction is for (e.g., "JOB", "CUSTOMER") - `merchantEntityId` (integer, nullable): Related entity ID (e.g., Job ID if transaction is for a job payment) ### GET /account/user **operationId:** `getAccountUser` **Summary:** Get current user profile Retrieves your user profile information including contact details, preferences, and notification settings. **Responses** - **200** — User profile retrieved successfully _The user behind the current credentials_ ```json { "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 } ``` - **401** — User is not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **404** — User not found — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. #### Schema: `AccountUserInfo` Complete user profile information for authenticated user. Excludes sensitive fields like passwords and internal role flags. - `id` (integer): User ID - `guid` (string): User GUID - `customerId` (integer): Customer/account ID this user belongs to - `customerName` (string, nullable): Customer/company name - `userName` (string, nullable): Login username - `firstName` (string, nullable): First name - `lastName` (string, nullable): Last name - `emailAddress` (string, nullable): Email address - `active` (boolean): Whether user is active (can log in) - `phone` (string, nullable): Phone number - `mobile` (string, nullable): Mobile number - `streetAddress` (string, nullable): Street address line 1 - `addressLine2` (string, nullable): Street address line 2 - `suburb` (string, nullable): Suburb/neighborhood - `city` (string, nullable): City - `state` (string, nullable): State/province - `postalCode` (string, nullable): Postal/ZIP code - `country` (string, nullable): Country - `locationID` (integer, nullable): Location ID (if multi-location enabled) - `locationName` (string, nullable): Location name (if multi-location enabled) - `defaultPage` (string, nullable): Default landing page URL - `orderNumberPrefix` (string, nullable): Custom order number prefix (max 5 chars) - `receiveEmails` (boolean): Receive general emails - `promotionRecipient` (boolean): Receive promotional emails - `receiveInvoice` (boolean): Receive invoice emails - `receiveInwards` (boolean): Receive inward shipment notifications - `textNotifyProof` (boolean): SMS notification when job proof sent - `textNotifyDispatch` (boolean): SMS notification when job dispatched - `usernameLoginOnly` (boolean): Whether this user logs in with username only (email is shared with another user) - `lastLogin` (string, nullable): Last login timestamp (UTC) - `loginCount` (integer): Total login count ### GET /account/users **operationId:** `getAccountUsers` **Summary:** Get list of users in organization 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. **Parameters** - `includeInactive` — in `query`: Include inactive users (default: false) - `page` — in `query`: Page number, 1-based (default: 1) - `pageSize` — in `query`: Number of records per page (default: 50) - `sortBy` — in `query`: Column to sort by (default: LastName). Valid values: LastName, FirstName, UserName, Email, LastLoginUtc, LoginCount - `sortDirection` — in `query`: Sort direction: ASC or DESC (default: ASC) - `textFilter` — in `query`: Optional text filter to search username, first name, last name, or email **Responses** - **200** — User list retrieved successfully _Users on the account (CustomerAdmin only)_ ```json { "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 } ] } ``` - **401** — User is not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **403** — User does not have admin privileges — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. #### Schema: `AccountUserSummary` - `pageSize` (integer): How many records one page can hold — the size requested, not the number returned. - `returnedResults` (integer): How many records this page actually contains. Lower than `pageSize` on the last page. - `totalResults` (integer): How many records match across every page. Use this for "N results", not the page length. - `totalPages` (integer): How many pages the full result set spans at the current `pageSize`. - `currentPage` (integer): Which page this is. One-based — the first page is 1, not 0. - `hasNext` (boolean): Whether another page follows. Prefer this over comparing page numbers yourself. - `hasPrevious` (boolean): Whether a page precedes this one. - `filter` (string, nullable): The filter applied to produce this result set, echoed back. Null when unfiltered. - `nextUrl` (string, nullable): Ready-made URL for the next page. Follow it rather than assembling your own — it carries the filter and page size already. Null on the last page. - `previousUrl` (string, nullable): Ready-made URL for the previous page. Null on the first page. - `entities` (array, nullable): The records themselves. Everything else on this object describes the page, not the data. #### Schema: `AccountUserSummary` Lightweight user summary for list view (GET /account/users). Provides essential user information for administrative user management. - `id` (integer): User ID - `guid` (string): User GUID - `userName` (string, nullable): Login username - `firstName` (string, nullable): First name - `lastName` (string, nullable): Last name - `emailAddress` (string, nullable): Email address - `active` (boolean): Whether user is active (can log in) - `locationName` (string, nullable): Location name (if multi-location enabled) - `usernameLoginOnly` (boolean): Whether this user logs in with username only (email is shared with another user) - `lastLogin` (string, nullable): Last login timestamp (UTC) - `loginCount` (integer): Total login count ### GET /account/users/{userId} **operationId:** `getAccountUsersByUserId` **Summary:** Get specific user details by user ID. Validates that the user belongs to the authenticated customer's organization. One user on your account. Requires the account administrator role; an ordinary user credential receives a 403. **Parameters** - `userId` _(required)_ — in `path`: User ID to retrieve **Responses** - **200** — Returns the user details _One user on the account (CustomerAdmin only)_ ```json { "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 } ``` - **401** — Unauthorized - user not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. - **403** — Forbidden - user does not have CustomerAdmin role — Authenticated, but this operation is not available to your account’s role. Some operations require the account administrator role; others are back-office only and are not part of this published surface. Do not retry — it will not start working. - **404** — Not found - user does not exist or does not belong to the customer — No such record, or it is not visible to your account. Credentials only ever see their own account’s data, so a valid identifier belonging to another customer answers exactly like one that does not exist. ## PromoCodes Validate a promotional code before applying it to a job. ### GET /promocode/validate/{code} **operationId:** `getPromocodeValidateByCode` **Summary:** Validate promo 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.) **Parameters** - `code` _(required)_ — in `path`: Promo code to validate - `processCodes` — in `query`: Optional comma-separated list of process codes (e.g., "Embroidery,Printing") from ProcessRank.Process **Responses** - **200** — Promo code validation result returned successfully (check isValid field) _A promotional code checked before it is applied to a job_ ```json { "isValid": true, "promoCode": "WINTER10", "description": null, "discount": 10, "isFixed": false, "message": "Promotion applied successfully.", "applicableProcessCodes": null } ``` - **401** — User is not authenticated — No valid bearer token. Request one from this region’s `/api/auth/token` with your `client_id` and `client_secret`, and send it as `Authorization: Bearer <token>`. A token from another region’s host will also produce this. #### Schema: `ValidatePromocodeResponse` Response from promo code validation - `isValid` (boolean): Whether the promo code is valid for the customer - `promoCode` (string, nullable): The promo code that was validated - `description` (string, nullable): Description of the promo code - `discount` (integer): Discount amount (cents if IsFixed=true, percentage if IsFixed=false) - `isFixed` (boolean): Whether this is a fixed amount discount (true) or percentage discount (false) - `message` (string, nullable): Error message if the promo code is not valid. Empty if valid. - `applicableProcessCodes` (array, nullable): List of applicable process codes from the ones provided (if processCodes parameter was supplied). Returns null if no processCodes were provided or if promo code is not process-specific. ## StockItemShipping Shipping options for stocked items, by stock item and market. ### GET /stock/{stockItemId}/shipping **operationId:** `getStockByStockItemIdShipping` **Summary:** Get all active shipping configurations for a stock item. How one stocked item ships — weight, dimensions, lead time and which warehouses it can ship from. **Parameters** - `stockItemId` _(required)_ — in `path`: The stock item ID **Responses** - **200** — List of shipping configurations _Shipping configuration for one stocked item_ ```json [ { "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": [] } ] ``` - **400** — Invalid stock item ID — The request was rejected before anything happened. Read the response body — validation failures come back as `{ "isValid": false, "validationMessages": [{ "errorCode", "message" }] }`. Retrying an unchanged request will fail identically. #### Schema: `StockItemShippingDto` Response DTO for a stock item shipping configuration, including resolved fulfilment location details from the junction table. - `id` (integer): Identifier for this shipping configuration row. - `stockItemId` (integer): The stocked item this configuration belongs to. - `markets` (array, nullable): Market codes this configuration applies in. An item can ship differently per market. - `shipMode` (string, nullable): How the item ships — for example bundled with the rest of the order, or as its own consignment. - `fulfilmentLocations` (array, nullable): The warehouses this item can ship from. - `weight` (number, nullable): Shipping weight, in `weightUnit`. Null where not captured, in which case rating falls back to defaults. - `weightUnit` (string, nullable): Unit for `weight` — `kg` or `lb`. - `length` (number, nullable): Package length, in `dimensionUnit`. - `width` (number, nullable): Package width, in `dimensionUnit`. - `height` (number, nullable): Package height, in `dimensionUnit`. - `dimensionUnit` (string, nullable): Unit for `length`, `width` and `height` — `cm` or `in`. - `fixedShipping` (number, nullable): A flat shipping charge that replaces live carrier rating for this item. Null means rate it normally. - `groundOnly` (boolean): True when the item cannot travel by air, which removes express methods from its options. - `estimatedLeadDays` (integer, nullable): Working days to expect before despatch, on top of any production time. - `active` (boolean): Whether this configuration is in use. Inactive rows are retained for history. - `createdAt` (string): When the configuration was created, UTC. - `updatedAt` (string): When the configuration was last changed, UTC. - `shipmentTypeOptions` (array, nullable): Per-method shipping overrides for this stock item in this market. Empty array if no overrides are configured. Order is meaningful — rows are returned in the sort order they were saved (first row may be treated by the consumer as the default method). #### Schema: `FulfilmentLocationDto` Nested DTO representing a resolved fulfilment location within a shipping config. - `locationCode` (string, nullable): Short code for the fulfilment location. - `locationName` (string, nullable): Human-readable name of the fulfilment location. - `locale` (string, nullable): The region the location sits in. - `shipFromCity` (string, nullable): City goods leave from — the origin used when rating shipping. - `shipFromCountryCodeIso2` (string, nullable): Two-character ISO country code goods ship from. #### Schema: `StockItemShipmentTypeOptionDto` A per-method shipping override for a stock item. Determines which shipment methods are available for this stock item in this market and what they cost. - `code` (string, nullable): Shipment-type code (e.g. "USPS", "Ground", "2 Day Air"). Must be a code configured in the tenant's `JobForm_DeliveryOptions` OrgPref. Matched case-insensitively; the server normalises to the canonical casing on persistence. - `name` (string, nullable): Display label shown to end-users. <b>Server-supplied</b> — populated from the matched `IDeliveryOption.Label` on write; client-sent values are ignored. Corresponds to the same Label exposed as `ShipmentType.Name` at `/account/shipment-settings`. - `price` (number, nullable): Price override. `null` = use rate-API price; `0` = free; positive = flat override. Always non-negative when set. ### GET /stock/shipping **operationId:** `getStockShipping` **Summary:** Get all active stock item shipping configurations for a specific market. Primary query used by Integrate for fetching all shipping metadata for a region. 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. **Parameters** - `market` — in `query`: The market code (e.g., US, AU, NZ, UK) **Responses** - **200** — List of stock item shipping configurations _Shipping configuration for every stocked item in a market_ ```json [ { "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": [] } ] ``` - **400** — Market is required — The `market` query parameter is required, even though the schema does not mark it as such. Pass a market code from the `markets` array on a stock item.