Concepts

# Core concepts

The things that will bite you if you skim. Read this once and the rest of the reference is obvious.

## Money is micros

A Money object is an integer count of millionths of the currency's major unit, plus an ISO 4217 code. One dollar is 1,000,000 micros. There are no floats anywhere in pricing, and the micro count travels as a string.

```json
{ "amountMicros": "12340000", "currency": "AUD" }   // $12.34
```

Divide by 1,000,000 for display, never for arithmetic. Do the maths in micros with integers and format at the edge.

## Every 64-bit number is a string

Including `amountMicros` and `orderNumber`. JavaScript loses precision above 2^53, so we never send those as JSON numbers. Quantities, discounts, percentages and markups are decimal strings too — a quantity of `"2.5"`, a discount of `"10.00"` meaning ten percent.

## Ids carry their type

Every id is a prefix, an underscore, and a 26-character sortable suffix. The prefix tells you what you are holding, and sending the wrong kind is rejected rather than silently mis-resolved.

`order_` · `quote_` · `line_` · `drawing_` · `cust_` · `contact_` · `label_` · `status_` · `prod_` · `prodrow_` · `rowprice_` · `pricelvl_` · `category_` · `kit_` · `kitrow_` · `kitcomp_` · `subkit_` · `flashing_` · `flashtpl_` · `material_` · `user_` · `company_` · `supplier_` · `whsub_` · `whdel_` · `whevt_` · `message_` · `attach_`

> When an accepted quote becomes an order, the suffix is kept and only the prefix changes: `quote_ABC…` becomes `order_ABC…`. That is how you follow one document across both feeds.

## Enums travel as names

Responses always return the upper-case name — `FULFILMENT_METHOD_DELIVERY`, not `2`. Integers are accepted on the way in, but send names. New values can appear inside v1, so treat an unrecognised name as unknown rather than crashing on it.

## Pagination

Every list takes `pageSize` and `pageToken` and returns `nextPageToken`. Loop until it comes back empty. Tokens are opaque, may be bound to the query that minted them, and should never be stored or constructed.

```js
let token = "";
do {
  const page = await get("/v1/orders", { pageSize: 100, pageToken: token });
  handle(page.orders);
  token = page.nextPageToken;
} while (token);
```

## Idempotency

Every create-style write — `CreateOrder`, `CreateQuote`, `CreateCustomer`, the add-line-item calls, and the message and attachment posts — takes a client-generated UUID in `requestId`. Requests carrying the same key are executed at most once: any repeat is rejected with a 409, and if the original request completed the error body's `resourceId` carries the id of the resource it created. Keys are scoped to the API key that sends them and retained for at least 24 hours.

> **A 409 is a stop sign, not a replay.** The API never returns the original response on a repeat, and a 409 also covers an original attempt whose outcome is unknown — in flight, failed, or cut off. Read the created id from `resourceId` in the error body, or verify with a read (`ListCustomers` with the `companyName` filter, comparing the full name, `GetOrder`, `GetQuote`), then send a fresh key if you still need a new document — a key is consumed by its first attempt, success or failure. If the idempotency store is unavailable, requests carrying a key are rejected with 503; requests without one are unaffected.

## Archiving

An archived document is hidden from every read by default, and stays read-only. Each endpoint opts back in differently, which is the part worth reading twice.

Where What the flag does
ListOrders?archived= Swaps which set you are listing. It is not a superset — `true` returns *only* archived orders.
GetOrder?includeArchived= Without it, an archived order is a 404. With it, you can read the order but still not write to it.
QueryOrders?includeArchived= Widens the feed to live **and** archived together. See the sync guide — this one is genuinely useful.

Every document read through one of those flags carries `isArchived: true`. On a default read it is always false. Changing the flag partway through pagination invalidates your page token, so start the walk again.

## Labels

A label is a small company-scoped tag — name and hex colour — that can sit on any number of quotes and orders. They are created and renamed in the Factory app; the API reads the list and sets which ones are attached. A rename keeps the id, so store the id and not the name.

> **Setting labels replaces the whole set.** `PUT /v1/orders/{orderId}/labels` is not an append. The list you send becomes the complete set — labels you leave out are removed, and an empty list clears every one. Read the order first if you mean to add to what is already there. An unknown label id fails the whole call with 404 and changes nothing.

You can also attach labels at creation time by passing `labelIds` on `CreateOrder` or `CreateQuote`, which saves a second call. Same validation: an unknown id fails the whole request with 404, before the document is created.

> **Labelling on create is not atomic.** In the rare case the document is created and the labelling then fails, the error names the new document id and the document exists **without** its labels. Do not retry the create — you would get a second document. Read the id out of the error and attach the labels with `SetOrderLabels`.

To find documents by label, pass `labelIds` to `ListOrders` or `ListQuotes`. It matches *any* of the ids you give rather than all of them, and combines with the other filters. Changing it mid-pagination invalidates your page token, same as the archive flag.

Labels come back on a full read of a document, oldest attachment first. They are absent from line-write responses, so read the document if you need to see them.

## Measurements price the line

On the lineal and square pricing strategies the priced amount comes from `measurements` — an array of measured pieces on the line's `pricing` object, and on each kit component. One entry is one piece size: `length`, `width` and `amount`.

```json
{
  "productKit": {
    "components": [
      {
        "productRowId": "prodrow_01hb7n3q5s7u9w1y3a5c7e9g1k",
        "pricingStrategy": "PRICING_STRATEGY_LINEAL_METRES",
        "measurements": [
          { "length": "2.4", "amount": "3" },
          { "length": "1.8", "amount": "1" }
        ]
      }
    ]
  }
}
```

All three values are decimal strings in the strategy's native unit — metres for the `*_METRES` strategies, feet for `*_FEET`. `width` belongs to the square strategies only — a width on a lineal entry is rejected, and a square entry without one is rejected. `amount` is how many pieces of that size and defaults to 1. On these strategies the measured amount is the priced quantity — `quantity` is not a factor. The one legacy exception is an on-the-fly line on an account without server-side derivation, where the old formula still multiplies by it. The server sums the pieces, applying your account's minimum-length setting on lineal strategies, to derive the amount that scales the line total.

`asBuiltMeasurements` sits beside `measurements` everywhere it appears — what production actually cut, in the same shape and units. As-builts drive cost, margin and stock consumption, never the line price, and whenever you omit them (create and update alike) they default to `measurements`. Send them only when the actual figures differ from what was quoted.

> **Kit components measure the same way.** Each component in `productKit.components` — and inside each of its `subKits` — carries its own `measurements` and `asBuiltMeasurements` pair, with the same rules as a line. The old contract's names are gone — `measurementItems` and `actualMeasurementItems` on lines, `initialMeasurementItems` and `sumLengths` on kit components — send the typed pair instead.

## Inventory is per-variant

A stock entry is the position of one product variant row at one colour, never a product total. A product with several rows appears once per row — which is why a single-row product can look like a product total — and a colour-tracked product splits each row again, one entry per colour. Sum a product's entries when you need the whole product's position. Each entry names its variant: `thickness` and the same `attributes` pairs the catalogue row holds, so entries are tellable apart without a catalogue read.

`available` is `onHand` minus `promised` and goes negative when more is committed than is on hand. All four quantities are decimal strings. `onHand` is the only value you can set — `promised` and `onTheWay` are computed from open orders and purchase orders, and `SetStockLevels` echoes the full recomputed set back.

> **The row id is the catalogue join key.** `productRowId` on a stock entry is the same id the catalogue product's `rows[].productRowId` carries — join there when you need the variant's prices or colour options. `colour` is populated only for colour-tracked products; all colour splits of a row share its thickness and attributes but carry their own quantities.

## Versioning

The major version is in the path. v1 is a stable contract: every published operation, field and value keeps its meaning and wire form for the life of the version. Inside v1 changes are additive and can ship at any time — new endpoints, new optional request fields and query parameters, new response fields, a list operation starting to populate a field it left empty, new enum values, new error types and codes, new webhook event types, limits raised or patterns widened, and new items marked Preview. Build for that: ignore fields you do not recognise, treat an enum value you do not recognise as “other” rather than failing, treat an unrecognised error type as a generic error of its HTTP status, keep page tokens opaque, and never parse an id beyond its prefix.

Breaking changes never ship inside v1: removing or renaming anything, adding a required request field, changing a type, a JSON name, a default or a meaning, changing which status or error type an outcome returns, making an always-present field conditional, reducing what a list operation populates, tightening validation on inputs that succeed today, or changing documented pagination or ordering. They arrive under a new version path or, for a removal, at the end of a deprecation period — announced in the changelog and marked in the description with its replacement and end date, and kept working for at least 12 months. When a new major version ships, v1 stays served for at least 12 months after it is generally available.

An endpoint or field whose description begins with “Preview:” is published for early use and is not yet covered by these guarantees; it can change or be withdrawn with 30 days’ notice in the changelog, and it graduates when the marker is removed. Catalogue search is in preview today. Key order, whitespace, the wording of messages, the contents of page tokens, concrete rate-limit numbers, response timing and any behaviour the documentation does not describe are never part of the contract.

---

Source: https://developer.factory.app/concepts · Factory Sales API v1
