# Fluit Public API > REST API for integrating external systems with Fluit ERP: webshops, WMS, EDI, > BI tools and custom integrations. The API is scoped to a single tenant (your company) — > the API key determines which company's data you read and write. - Base URL: `https://api.erp.fluit.cloud` — the paths listed below already include the `/preview` prefix. - Authentication: send your API key in the `X-Api-Key` header on every request. Keys are created in Fluit under Settings → API keys. - Version: `preview`. - OpenAPI document, with the complete request and response schemas: https://api.erp.fluit.cloud/swagger/public-preview/swagger.json - Reference documentation for humans: https://fluit.se/api-reference - Contact: info@fluit.se ## Getting started 1. Create an API key in Fluit under **Settings → API keys**. The key is shown once — store it securely. Keys are prefixed `fluit_live_sk_` (production) or `fluit_test_sk_` (test). 2. Call the API with the key in the `X-Api-Key` header: ```bash curl https://api.erp.fluit.cloud/preview/items?pageSize=5 \ -H "X-Api-Key: fluit_live_sk_..." ``` 3. Create your first order (note the required `Idempotency-Key` on POST): ```bash curl -X POST https://api.erp.fluit.cloud/preview/orders \ -H "X-Api-Key: fluit_live_sk_..." \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "customerNumber": "CUST-001", "lines": [ { "itemNumber": "WIDGET-A", "quantity": "10" } ] }' ``` ## Conventions - **Business keys, not GUIDs.** Resources are addressed by their business keys, and `*Code` is used for reference data (warehouses, payment terms, …). List the valid codes via the Reference endpoints. | Resource | Key in the URL | | --- | --- | | Customers | `customerNumber` | | Items | `itemNumber` | | Sales orders | `orderNumber`, lines by `lineNumber` | | Suppliers | `supplierNumber` | | Purchase orders | `orderNumber`, lines by `lineNumber` | Sales orders and purchase orders live under different paths (`/orders` and `/purchase-orders`), so an order number is only ever ambiguous across the two if you reuse the same number series for both. - **Decimals are strings.** All monetary amounts and quantities are serialised as decimal strings (`"123.45"`, invariant format) to avoid IEEE 754 floating-point errors. Requests accept both strings and numbers; responses always return strings. Check the item's `decimalPlaces` for how many decimals a quantity may have. - **Dates and times.** Dates are ISO 8601 (`2026-06-11`). Timestamps are UTC, but are currently serialised **without a timezone designator** (`2026-06-09T22:07:47.7639194`, no trailing `Z`). Most date parsers read that as *local* time — `new Date(...)` in JavaScript and `datetime.fromisoformat(...)` in Python both produce a naive or local-shifted value. Treat every response timestamp as UTC explicitly rather than letting the parser guess. Request parameters do not have this problem: `modifiedSince` accepts `Z`, a numeric offset or no designator and resolves all three to the same instant. - **Fields are nullable unless the schema says otherwise.** This bites on fields that look mandatory: `baseUnitCode` is null for items with no unit configured, `salesPrice` is null for items with no list price, and `modifiedDate` is null for records that have never been changed since creation. When syncing on `modifiedDate`, fall back to `createdDate`. - **Enums are strings.** Status fields and similar are serialised as enum names (`Placed`, `Shipped`, …) and documented per field. Enum values in query-string filters are matched case-insensitively; an unknown value returns `400` rather than an empty result. Treat enum values as open — new ones may be added without a version bump. - **Resources link to themselves.** Every resource carries `links.self`, the canonical URL of the resource. `POST` responses return the same representation as the corresponding `GET`, with the URL in the `Location` header as well. - **PATCH is JSON Merge Patch.** Only fields present in the body are updated. Pass `null` to clear a nullable field; omitted fields are left unchanged. Unknown fields are silently ignored. - **Country codes** are ISO 3166-1 alpha-2 (`SE`), **currency codes** ISO 4217 (`SEK`). ## Prices and stock Two fields on the item representation are routinely mistaken for something they are not. Both mistakes are silent — you get a plausible number, not an error. - **`salesPrice` is the item's list price, not the price anyone pays.** It ignores price lists, customer agreements, campaigns and volume breaks. Call `GET /preview/items/{itemNumber}/price` to get the price an order line would actually receive, with `?customerNumber=` for customer-specific pricing and `?quantity=` for volume tiers. The two commonly differ by double-digit percentages. Use `salesPrice` only where you genuinely want an uncontracted reference price. - **The item list carries no stock.** `GET /preview/items` returns no quantity at all; availability lives on `GET /preview/items/{itemNumber}/availability`, one call per item. There is no bulk availability endpoint, so a catalogue sync with stock costs 1 + N requests against the per-minute quota. Fetch availability only for the items you are about to display or order, rather than walking the whole catalogue, and remember `availableQuantity` (on hand minus reservations) is the number you can promise — not `quantityOnHand`. ## Calling from a browser The API sends `Access-Control-Allow-Origin: *` and allows `x-api-key` in the preflight, so a browser page can call `/preview` directly with no proxy. That is deliberate, and it suits internal tools and prototypes. It does not make the key safe in client code. Anything in a page's JavaScript is readable by every visitor, and a leaked key grants full access to the tenant's data. For anything user-facing, keep the key on a server you control and let the browser talk to that server instead — or have each user supply their own key at runtime. ## Flows ### Purchasing: order and receive goods 1. `POST /preview/purchase-orders` with `supplierNumber` and lines. The order is created in `Draft` and nothing is sent to the supplier yet. Omit `unitPrice` to use the supplier price list, and `unit` to use the item's base unit. 2. `POST /preview/purchase-orders/{orderNumber}/send` moves it to `Sent`. By default this only records that the order went out — pass `{"sendEmail": true}` if you want Fluit to email the PDF to the supplier rather than sending it yourself over EDI. 3. `POST /preview/purchase-orders/{orderNumber}/confirm` when the supplier confirms. If they came back with different quantities or dates, `PATCH` the affected lines first. 4. `POST /preview/purchase-orders/{orderNumber}/lines/{lineNumber}/receive` as goods arrive. Each call books stock, creates an inventory transaction and advances the line and order to `PartiallyReceived` and then `Received`. Call it once per delivery for partial deliveries. 5. `POST /preview/purchase-orders/{orderNumber}/close` if the supplier will not deliver the remainder — that settles the order without waiting for the outstanding quantity. Receiving is not reversible through this API, so retries matter: a repeated call with the same `Idempotency-Key` replays the original response instead of booking the goods twice. ### Configurator: sell a made-to-measure product Some items are not picked off a shelf — they are built to the customer's measurements and choices. Curtains, blinds and awnings are the archetype: width and height drive the fabric consumption, the fabric and the control type drive the price, and no two orders are alike. These items are ordered through `/preview/configurations` rather than as a plain order line, so the choices survive into production. 1. `GET /preview/items?isConfigurable=true` finds the items that have a configurator. 2. `GET /preview/items/{itemNumber}/configuration` returns the whole form definition in one call: the features, their input types and bounds, and the selectable options. Each feature's `featureType` tells you which field to send back — `Number` → `number`, `Selection` → `optionCode`, `Boolean` → `boolean`, `ItemSelection` → `itemNumber`, `Text` → `text`. `Calculated` features take no input. 3. `POST /preview/configurations/calculate` on every change while the customer configures. It returns the price for the current choices plus `resolvedValues` — the derived numbers such as area and fabric consumption, so you do not have to reimplement the formulas. An incomplete configuration is a normal state, not an error: it comes back as `200` with `isValid: false` and every problem listed in `validationErrors`, ready to show at the right field. This is the one POST that does not require an `Idempotency-Key`. 4. `POST /preview/configurations` once the customer is happy. The response carries a `configurationNumber` — the business key for everything that follows — and the price. `customerNumber` may be left out here and attached later, which is what a storefront that configures before asking who the customer is needs. 5. `POST /preview/configurations/{configurationNumber}/reconfigure` to change it later. Every field is optional: omit `values` to keep the choices and change only the quantity, pass `customerNumber` to claim an anonymous configuration. When `values` *is* present it replaces the whole set. The response is the updated, repriced configuration. 6. `POST /preview/configurations/{configurationNumber}/order` turns it into a sales order and returns the order. Add freight or more lines through the order endpoints, then `POST /preview/orders/{orderNumber}/place`. Behind the line, the configuration becomes a work order carrying the exploded bill of materials and routing, so production knows what to build. A configurator produces abandoned sessions: `DELETE /preview/configurations/{configurationNumber}` discards one that was never ordered. A configuration that has become an order is frozen — reconfiguring or deleting it returns `409`, and it disappears from `GET /preview/configurations` without leaving a tombstone, so store the `orderNumber` from the /order response if you keep local copies. #### Pricing a configuration The price is the item's own price from the price hierarchy (customer price lists, agreements, campaigns, volume breaks) plus the configuration surcharge. Three things are worth knowing before you build against it: - **It is a live price, not a locked quote.** Both `calculate` and the order conversion run the price engine at the moment they are called, so a campaign that starts or expires in between moves the price. The order always uses the customer's currency; a `currencyCode` passed to `calculate` affects that calculation only. - **Automatic customer discounts do not apply.** The composed price is set as a manual unit price on the line, which bypasses the discount engine. Price lists, agreements, campaigns and volume breaks are already reflected in the base price. - **A choice that links an item does not change the price by itself.** When an option has a `linkedItemNumber`, that item is added to the bill of materials as a cost line and the option's `priceImpact` is deliberately ignored. To make a choice cost more, give it a `priceImpact` without a linked item, or drive the price from a `Calculated` feature. ## Pagination and syncing List endpoints are paginated with `?page=` (1-based) and `?pageSize=` (default 50, max 200) and respond with: ```json { "items": [], "totalCount": 0, "page": 1, "pageSize": 50, "totalPages": 0, "hasPreviousPage": false, "hasNextPage": false } ``` For incremental sync, poll with `?modifiedSince=` (UTC ISO 8601): store the timestamp at which you started the previous sync and pass it on the next run. Both created and modified records are returned. ## Idempotency All POST requests require an `Idempotency-Key` header — a unique value (UUID recommended, max 255 characters) per logical attempt. The one exception is `POST /preview/configurations/calculate`, which has no side effects and is a POST only because its input does not fit in a URL: - **Retrying with the same key and body** returns the original response unchanged (marked with the `Idempotency-Replayed: true` header) instead of e.g. creating a duplicate order after a network timeout. - **Reusing a key** for a different endpoint or body returns `422 Unprocessable Entity` with `code: "idempotencyKey.conflict"`. - **Concurrent requests** with the same key are serialised; if the first is still running after 30 s the second receives `503` with a `Retry-After` header. - Stored responses expire after **24 hours**. Generate a new key for every new logical request; reuse the key only when retrying the same request. ## Errors Errors follow RFC 7807 (`application/problem+json`): ```json { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "customerNumber", "detail": "Customer 'CUST-999' not found.", "status": 400 } ``` | Status | Meaning | | ------ | ------- | | 400 | Validation error — an unknown business key, an invalid request body field, or an unknown enum value in a query filter | | 401 | Missing or invalid API key | | 403 | The API key is valid but not permitted to perform the operation | | 404 | The resource in the URL does not exist | | 409 | The operation conflicts with the resource's state (e.g. cancelling a shipped order) | | 413 | `POST` body larger than 1 MB, when the request declares a `Content-Length` | | 422 | Idempotency-Key reused for a different request | | 429 | Rate limit exceeded — back off per the `Retry-After` header | | 500 | Unexpected server error. Safe to retry with the same `Idempotency-Key` — server errors are never replayed from cache | Field-level problems are listed in the `errors` extension array, one entry per violation with a `code` (the field or business rule) and a `description`. Request body constraints published in this document — required fields, maximum lengths, patterns and ranges — are enforced; a violation returns `400` with the offending field in `errors`. Nested fields use a dotted path, e.g. `lines[0].discountPercent`. Two cases fall outside this shape and return a plain `400` without a problem+json body: a request body that is not well-formed JSON, and a query parameter other than an enum filter (`page`, `pageSize`, `quantity`, dates and `modifiedSince`) that cannot be parsed into its declared type. Enum filters such as `?status=` are parsed by the API itself and do produce the `errors` array. ## Rate limiting Requests are limited to **1000 per minute per API key** (fixed window). `429` responses carry `Retry-After` in seconds — honour it rather than retrying on a fixed delay. Where the quota headers `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` are present they describe the current window, but they are not emitted on every deployment. Treat them as advisory: read them when they are there, and never make your backoff conditional on finding them. Code that only slows down once `X-RateLimit-Remaining` gets low will otherwise run flat out into a `429`. ## Preview status The API is mounted under `/preview` and is in preview: the schema can change without notice until the stable `/v1` release. Breaking changes are listed in the changelog. Questions or access requests: [info@fluit.se](mailto:info@fluit.se). ## Endpoints 57 endpoints. Each one is listed with its query parameters, the request body examples from the OpenAPI document and the status codes it can return. Field-level schemas for request and response bodies are in the OpenAPI document linked above. ### Customers Customers with their contact persons and delivery addresses. Addressed by customerNumber. #### `GET /preview/customers` — List customers Returns a paginated list of customers for the authenticated tenant. Filter by ?search=, ?isActive=true|false, ?modifiedSince=. Sorted by customer number. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `search` (string), `isActive` (boolean), `modifiedSince` (date-time), `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401` #### `POST /preview/customers` — Create a customer Creates a new customer for the authenticated tenant. Customer number is auto-generated if not provided. defaultBackorderBehavior controls what happens to unfulfillable quantities on this customer's orders; omit it to use CreateBackorder. Allowed values: CreateBackorder, CancelRemaining, HoldOrder. The response body is the same representation as GET /preview/customers/{customerNumber}; the canonical URL is returned in the Location header and in links.self. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Create customer: ```json { "name": "Acme AB", "customerNumber": "CUST-001", "organizationNumber": "5560001234", "invoiceEmail": "invoice@acme.se", "phone": "+46701234567", "street1": "Storgatan 1", "postalCode": "11122", "city": "Stockholm", "countryCode": "SE" } ``` Responses: `201`, `400`, `401` #### `GET /preview/customers/{customerNumber}` — Get a customer by customer number Returns the details of a customer belonging to the authenticated tenant. Responses: `200`, `401`, `404` #### `PATCH /preview/customers/{customerNumber}` — Update a customer Partially updates a customer. Only provided fields are updated (JSON Merge Patch semantics). Omitted fields are left unchanged. Pass null to clear a nullable field. Unknown fields are silently ignored. Request body: `application/json`, `application/merge-patch+json` (required) Responses: `204`, `400`, `401`, `404` #### `GET /preview/customers/{customerNumber}/addresses` — List customer delivery addresses Returns a paginated list of the delivery addresses registered on the customer, default address first. Use the fields to populate deliveryAddress when creating orders. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401`, `404` #### `GET /preview/customers/{customerNumber}/contacts` — List customer contacts Returns a paginated list of the contact persons registered on the customer, default contact first. Use a contact's id as customerContactId when creating orders. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401`, `404` ### Items Items/products including stock availability, price calculation, units, attributes and assets. Addressed by itemNumber. #### `GET /preview/items` — List items Returns a paginated list of items for the authenticated tenant. Filter by ?search=, ?status= (Draft, PendingApproval, Active, PhasingOut, Discontinued, Archived; matched case-insensitively, an unknown value returns 400), ?modifiedSince=, ?isConfigurable=, ?categoryCode=. Sorted by item number. ?modifiedSince= returns both created and modified items. ?isConfigurable=true returns only made-to-order items that have a configuration schema (GET /preview/items/{itemNumber}/configuration); false returns only the plain catalogue items. ?categoryCode= is an exact match on the stable category code and only counts active, non-deleted assignments. Use it to build the picker for an ItemSelection feature: pass the feature's selectableItemCategoryCode from the configuration schema. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `search` (string), `status` (string; one of `Draft`, `PendingApproval`, `Active`, `PhasingOut`, `Discontinued`, `Archived`), `modifiedSince` (date-time), `isConfigurable` (boolean), `categoryCode` (string), `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `400`, `401` #### `POST /preview/items` — Create an item Creates a new item for the authenticated tenant. Item number is auto-generated if not provided. The response body is the same representation as GET /preview/items/{itemNumber}; the canonical URL is returned in the Location header and in links.self. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Minimal — name only: ```json { "name": "Widget Pro 3000" } ``` Example request body — Full item with pricing and customs: ```json { "name": "Widget Pro 3000", "itemNumber": "WIDGET-PRO", "description": "High-quality widget for professional use.", "barcode": "7350100500001", "salesPrice": "299.00", "costPrice": "120.00", "hsCode": "84795000", "countryOfOrigin": "SE" } ``` Responses: `201`, `400`, `401` #### `GET /preview/items/{itemNumber}` — Get an item by item number Returns the full details of an item by its unique item number: all orderable units with barcodes and conversion factors (base unit first), specification attributes, category assignments, cross-references (MPN/OEM/extra barcodes) and, for variants, the parent item and variant attribute values. The list endpoint returns a leaner representation without these collections. Responses: `200`, `401`, `404` #### `PATCH /preview/items/{itemNumber}` — Update an item Partially updates an item. Only provided fields are updated (JSON Merge Patch semantics). Omitted fields are left unchanged. Pass null to clear a nullable field. Unknown fields are silently ignored. itemNumber cannot be changed here — it is the resource's public key and the address external systems reference. Request body: `application/json`, `application/merge-patch+json` (required) Responses: `204`, `400`, `401`, `404` #### `GET /preview/items/{itemNumber}/assets` — List item assets Returns a paginated list of the item's public images and documents, primary image first and then by sort order. Only assets marked as public are included. url points either to an external location or to GET /preview/items/{itemNumber}/assets/{assetId}/download (same API key required). Use languageCode to pick language-specific assets where present. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401`, `404` #### `GET /preview/items/{itemNumber}/assets/{assetId}/download` — Download an item asset Streams the asset file (image or document). Externally hosted assets return a 302 redirect to the external URL. Only assets marked as public can be downloaded. Responses: `200`, `302`, `401`, `404` #### `GET /preview/items/{itemNumber}/availability` — Get item stock availability Returns the item's stock availability summed across all active warehouses and broken down per warehouse. availableQuantity is the quantity that can be promised to new orders (on hand minus reservations). Items never stocked in a warehouse return an empty warehouses list with zero totals. The warehouse breakdown is bounded by the tenant's warehouse count and is not paginated. Responses: `200`, `401`, `404` #### `GET /preview/items/{itemNumber}/price` — Calculate item price Calculates the price for an item via the price engine — the same price an order line would get. Pass ?customerNumber= for customer-specific pricing (price lists, agreements, campaigns); omit it for the default price. ?quantity= (default 1) activates volume pricing, ?currencyCode= overrides the currency (defaults to the customer's or the tenant base currency). quantityBreaks lists the volume price tiers of the applied price list. Query parameters: `customerNumber` (string), `currencyCode` (string), `quantity` (number; default `1`) Responses: `200`, `400`, `401`, `404` ### SalesOrders Sales orders with lines, fulfilment status and shipments. Addressed by orderNumber. Draft orders are not visible. #### `GET /preview/orders` — List sales orders Returns a paginated list of sales orders for the authenticated tenant. Draft orders are excluded. Filter by ?status= (Placed, Released, Picking, Packed, Shipped, Completed, Cancelled; matched case-insensitively, an unknown value returns 400), ?customerNumber=, ?search=, ?orderDateFrom=, ?orderDateTo=, ?modifiedSince= (ISO 8601 UTC datetime). Sorted by order date descending. ?search= performs a case-insensitive partial match against both orderNumber and customerReference. Example: ?search=ORD-2024 matches orders whose order number or customer reference contains 'ORD-2024'. ?modifiedSince= returns both created and modified orders. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `status` (string; one of `Placed`, `Released`, `Picking`, `Packed`, `Shipped`, `Completed`, `Cancelled`), `customerNumber` (string), `orderDateFrom` (date), `orderDateTo` (date), `search` (string), `modifiedSince` (date-time), `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `400`, `401` #### `POST /preview/orders` — Create a sales order Creates a new sales order for the authenticated tenant. customerNumber and at least one line are required (or set createAsDraft=true to create an empty draft); all other fields default from the customer or tenant settings. backorderBehavior: omit or null to inherit the tenant/customer configured default. Allowed values: CreateBackorder, CancelRemaining, HoldOrder. orderNumber: if omitted a number is auto-generated from the tenant number sequence; if provided and already in use, 409 Conflict is returned. Use deliveryAddress to override the delivery address inline; if omitted the customer's default address is used. Optionally include lines to create order lines atomically with the order. Set createAsDraft=true to keep the order in Draft status (e.g. to add more lines via POST /orders/{orderNumber}/lines before placing). The response body is the same representation as GET /preview/orders/{orderNumber}; the canonical URL is returned in the Location header and in links.self. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Minimal — customer number and one line: ```json { "customerNumber": "CUST-001", "lines": [ { "itemNumber": "WIDGET-A", "quantity": "10" } ] } ``` Example request body — Order with lines and delivery address: ```json { "customerNumber": "CUST-001", "orderDate": "2026-05-25", "currencyCode": "SEK", "customerReference": "PO-2026-042", "lines": [ { "itemNumber": "WIDGET-A", "quantity": "10", "unit": "st" }, { "itemNumber": "WIDGET-B", "quantity": "5", "unitPrice": "299.00" } ], "deliveryAddress": { "name": "Acme AB", "street1": "Storgatan 1", "postalCode": "11122", "city": "Stockholm", "countryCode": "SE" } } ``` Responses: `201`, `400`, `401`, `409` #### `GET /preview/orders/{orderNumber}` — Get a sales order by order number Returns the details of a sales order by its unique order number. Responses: `200`, `401`, `404` #### `POST /preview/orders/{orderNumber}/cancel` — Cancel a sales order Cancels a sales order. Only orders that have not been shipped can be cancelled. Returns 409 Conflict if the order is in a state that does not permit cancellation. Headers: `Idempotency-Key` (string; required) Responses: `204`, `401`, `404`, `409` #### `POST /preview/orders/{orderNumber}/lines` — Add a line to a sales order Adds an order line to an existing sales order. If unitPrice is omitted the price is calculated automatically via the price engine. discountPercent must be between 0 and 100; it overrides the auto-calculated discount, omit it to apply the discount engine rules. The Location header points at the new line's address, orders/{orderNumber}/lines/{lineNumber}. Returns 409 Conflict if the order is in a state that does not permit new lines. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Add order line: ```json { "itemNumber": "WIDGET-B", "quantity": "5", "unit": "st", "unitPrice": "299.00" } ``` Responses: `201`, `400`, `401`, `404`, `409` #### `PATCH /preview/orders/{orderNumber}/lines/{lineNumber}` — Update an order line Partially updates an order line identified by its line number. Only provided fields are updated (JSON Merge Patch semantics); omitted fields are left unchanged. quantity and unitPrice cannot be null; pass null for discountPercent, requestedDeliveryDate or notes to clear them. Line totals and order totals are recalculated automatically. Returns 409 Conflict if the order is completed/cancelled, if delivery has started on the line, or if the line has been split into sub-lines. Request body: `application/json`, `application/merge-patch+json` (required) Responses: `204`, `400`, `401`, `404`, `409` #### `DELETE /preview/orders/{orderNumber}/lines/{lineNumber}` — Delete an order line Removes a line from a sales order and releases any stock reservations for it. Order totals are recalculated automatically. Returns 409 Conflict if the order is completed/cancelled or the line has been split into sub-lines. Responses: `204`, `401`, `404`, `409` #### `POST /preview/orders/{orderNumber}/place` — Place a sales order Transitions a sales order from Draft to Placed, confirming it for processing. Returns 409 Conflict if the order is not in Draft status. Returns 400 Bad Request for business-rule violations; the Problem Details response includes an `errors` extension array with one entry per violation, each containing a `code` and `description`. Known error codes: `SalesOrder.NoLinesInOrder` (order must have at least one line before it can be placed). Headers: `Idempotency-Key` (string; required) Responses: `204`, `400`, `401`, `404`, `409` #### `GET /preview/orders/{orderNumber}/shipments` — List shipments for a sales order Returns a paginated list of the shipments fulfilling the order, including carrier tracking numbers once booked. An order can have multiple shipments (partial deliveries) and a shipment can cover multiple orders (consolidated delivery) — only lines belonging to this order are included. Returns an empty items list if fulfilment has not started. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401`, `404` ### Configurations Made-to-order products: read an item's configuration schema, price and validate a set of choices, save it as a configuration and turn it into a sales order. Addressed by configurationNumber. The schema endpoint lives at /preview/items/{itemNumber}/configuration because it belongs to the item, but it is grouped here so the whole configurator flow reads in one place. #### `GET /preview/configurations` — List saved configurations Returns the configurations that have not yet become orders, oldest configuration number first. A configuration disappears from this list once POST /preview/configurations/{configurationNumber}/order has turned it into a sales order — from that point it is an order and is read through GET /preview/orders instead. **Removals are not visible here.** `?modifiedSince=` (ISO 8601 UTC) returns configurations created or changed since that instant, but one that has been ordered or deleted simply stops appearing — there is no tombstone, so a poller that only reads the delta keeps a stale copy forever. Record the `orderNumber` from the response to the /order call, and reconcile against GET /preview/orders or a full re-read when you need to retire local copies. Other filters: `?search=` matches configuration number, title and description (case-insensitive, partial); `?customerNumber=` and `?itemNumber=` are exact matches. The price fields (`unitPrice`, `totalPrice`, …) are **null** in this list — pricing a whole page would mean one price engine run per row. Read a single configuration, or use POST /preview/configurations/calculate, when you need prices. Paginated response: `{ items, totalCount, page, pageSize, totalPages, hasPreviousPage, hasNextPage }`. `?page=` defaults to 1, `?pageSize=` to 50 and is capped at 200. Query parameters: `search` (string), `customerNumber` (string), `itemNumber` (string), `modifiedSince` (date-time), `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401` #### `POST /preview/configurations` — Save a configuration Persists a set of choices as a configuration and returns it with a `configurationNumber` — the business key for every follow-up call. Use this once the customer has settled on a configuration that POST /preview/configurations/calculate reports as valid; the calculate endpoint is for the live pricing while they are still choosing. The configuration is validated on save: a missing required value, a number outside its bounds or an option that does not belong to its feature returns `400` with the error code in the `errors` array (`FeatureExplosion.RequiredFeatureMissing`, `FeatureExplosion.ValueOutOfRange`, `FeatureExplosion.InvalidOption`). Validate with calculate first to get all problems at once. `customerNumber` is optional here so an anonymous session can be saved and claimed later, but it must be set before POST /preview/configurations/{configurationNumber}/order will succeed. Add it afterwards by passing `customerNumber` to POST /preview/configurations/{configurationNumber}/reconfigure. The response carries the priced configuration — `unitPrice` and `totalPrice` alongside the choices. Requires an `Idempotency-Key` header like every other POST — reuse the same key on a retry to get the original configuration back instead of creating a duplicate. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Minimal — item and quantity only: ```json { "itemNumber": "CURTAIN-PLEAT", "quantity": "1" } ``` Example request body — Full — a measured curtain for a named customer: ```json { "itemNumber": "CURTAIN-PLEAT", "quantity": "2", "customerNumber": "CUST-001", "title": "Living room curtains", "description": "Window 2, measured 2026-07-30", "warehouseCode": "MAIN", "values": [ { "featureCode": "WIDTH", "number": "240" }, { "featureCode": "HEIGHT", "number": "180" }, { "featureCode": "FABRIC", "optionCode": "LINEN-NATURAL" }, { "featureCode": "LINING", "boolean": true }, { "featureCode": "RAIL", "itemNumber": "RAIL-3M" } ] } ``` Responses: `201`, `400`, `401` #### `POST /preview/configurations/calculate` — Price and validate a configuration Runs the configuration through the same engine and the same price hierarchy a real order uses, without saving anything. Call it on every change while the customer configures. **This is a live price, not a locked quote.** The order re-prices when the configuration is converted, so the same choices give the same `unitPrice` only while the underlying prices hold: a campaign that starts or expires in between, or an agreement that changes, moves the price. The order also always uses the customer's own currency — a `currencyCode` passed here affects the calculation only. Persisting an agreed price is not supported yet; treat a quoted price as valid for the moment it was calculated. **An incomplete configuration is not an error.** Missing required values, numbers outside `minValue`/`maxValue` and options that do not belong to their feature all come back as `200` with `isValid: false` and every problem listed in `validationErrors` (`featureCode`, `code`, `message`), so the app can show them at the right field. The price fields are null in that case. Only structural problems — unknown `itemNumber`, `customerNumber`, `featureCode` or `optionCode` — return `400`. `resolvedValues` carries the derived numbers (area, fabric consumption, …) computed from `Calculated` features, so the app does not have to reimplement the formulas. Pass `?includeBreakdown=true` to also get `components[]` (the exploded bill of materials) and `operations[]` (the production steps). It is off by default — that is costing data, and a consumer-facing configurator should not ship it to the browser. `unitPrice` excludes VAT, freight and order-level discounts. Note that a configured line is priced as a manual unit price, so automatic customer discounts (the discount engine) do not apply to it; price lists, agreements, campaigns and volume breaks do. A choice whose option carries a `linkedItemNumber` adds that item to the bill of materials but does **not** change `unitPrice` — see the note on `priceImpact` in the configuration schema. This endpoint has no side effects and is the one POST under /preview that does **not** require an `Idempotency-Key` header. Query parameters: `includeBreakdown` (boolean; required) Request body: `application/json` (required) Example request body — Minimal — just the item and a quantity: ```json { "itemNumber": "CURTAIN-PLEAT", "quantity": "1" } ``` Example request body — Full — a made-to-measure curtain for a specific customer: ```json { "itemNumber": "CURTAIN-PLEAT", "quantity": "2", "customerNumber": "CUST-001", "currencyCode": "SEK", "values": [ { "featureCode": "WIDTH", "number": "240" }, { "featureCode": "HEIGHT", "number": "180" }, { "featureCode": "FABRIC", "optionCode": "LINEN-NATURAL" }, { "featureCode": "LINING", "boolean": true }, { "featureCode": "RAIL", "itemNumber": "RAIL-3M" }, { "featureCode": "LABEL", "text": "Living room, window 2" } ] } ``` Responses: `200`, `400`, `401` #### `GET /preview/configurations/{configurationNumber}` — Get a saved configuration Returns one configuration with its chosen values, resolved to feature and option codes, and priced for its customer — `basePricePerUnit`, `configurationSurchargePerUnit`, `unitPrice` and `totalPrice`. Use it to resume a saved configuration without replaying the values through POST /preview/configurations/calculate. Returns `404` once the configuration has become an order — a configuration only exists until it is ordered. Read the resulting order through GET /preview/orders/{orderNumber} instead, using the order number from the response to POST /preview/configurations/{configurationNumber}/order. The price is calculated on read, so it reflects today's price lists and campaigns rather than what was quoted when the configuration was saved. The price fields are omitted if the saved configuration can no longer be priced (e.g. the item's features have changed since). Responses: `200`, `401`, `404` #### `DELETE /preview/configurations/{configurationNumber}` — Discard a configuration Deletes a configuration that was never ordered. A configurator produces a lot of abandoned configurations, and this is how the app cleans them up instead of leaving them in the tenant's list. Returns `409` (`Configuration.NotConfigurable`) once the configuration has become an order — what sits behind the order line is production data and is not deletable through this API. Cancel the order with POST /preview/orders/{orderNumber}/cancel instead. Responses: `204`, `401`, `404`, `409` #### `POST /preview/configurations/{configurationNumber}/order` — Turn a configuration into a sales order Creates a sales order with a single line for the configured product, priced at base price plus configuration surcharge through the same price hierarchy POST /preview/configurations/calculate uses. The response is the same representation as GET /preview/orders/{orderNumber}, with the canonical URL in the `Location` header. The line is priced **now**, not from a stored quote: the price engine runs again at conversion, in the customer's own currency. Unchanged choices give the calculated price as long as the underlying prices still hold — a campaign starting or expiring in between will move it. The order is created in Draft. Add freight, extra lines or a delivery address through the order endpoints, then place it with POST /preview/orders/{orderNumber}/place. Behind the line, the configuration becomes a production work order carrying the exploded bill of materials and routing. The configuration itself is consumed: it stops appearing in GET /preview/configurations and its own GET returns `404` afterwards. Store the `orderNumber` from this response — it is the only link back to the configuration you just converted. Known error codes: `409 WorkOrder.NotEstimate` — already ordered. `409 WorkOrder.NoCustomer` — the configuration has no customer; attach one by passing `customerNumber` to reconfigure first. `409 WorkOrder.NoOutputItem` — the configuration has no item. `400 Warehouse.NoDefault` and `400 OrderType.NoDefault` — the tenant has no default warehouse or order type configured; that is a setup problem in Fluit, not something the request can fix. Note that the line is priced as a manual unit price, so automatic customer discounts do not apply to it. Price lists, agreements, campaigns and volume breaks are already reflected in the base price. Headers: `Idempotency-Key` (string; required) Request body: `application/json` Example request body — No body — order with an open delivery date: ```json {} ``` Example request body — With a requested delivery date: ```json { "requestedDeliveryDate": "2026-09-15" } ``` Responses: `201`, `400`, `401`, `404`, `409` #### `POST /preview/configurations/{configurationNumber}/reconfigure` — Change a saved configuration Changes a saved configuration and re-runs the engine. Every field is optional and omitting one leaves that part alone: - `values` **omitted** keeps the current choices — that is how you change only the quantity. When present it is a **full replacement**, not a patch: anything not in the list is cleared, and an empty array clears every choice. - `quantity` omitted keeps the current quantity. - `customerNumber` omitted keeps the current customer. Set it to claim a configuration that was saved anonymously — a configuration must have a customer before it can be ordered. Returns `200` with the updated configuration, including the recalculated `unitPrice`, rather than `204`. That is a deliberate departure from the usual action-endpoint convention — the whole point of reconfiguring is the new result, and forcing a follow-up GET for it would be wasteful. Returns `409` (`Configuration.NotConfigurable`) once the configuration has become an order: an ordered configuration is frozen, since changing it would silently change what the customer bought. The status is checked before the body, so an ordered configuration returns the conflict regardless of what the payload contains. Change the order line instead, or create a new configuration. Invalid values return `400` with the offending `FeatureExplosion.*` code. Use POST /preview/configurations/calculate first to see all problems at once. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Change the quantity only — the choices are kept: ```json { "quantity": "3" } ``` Example request body — Attach a customer to an anonymously saved configuration: ```json { "customerNumber": "CUST-001" } ``` Example request body — New measurements and a different fabric: ```json { "quantity": "2", "values": [ { "featureCode": "WIDTH", "number": "260" }, { "featureCode": "HEIGHT", "number": "180" }, { "featureCode": "FABRIC", "optionCode": "VELVET-DEEPBLUE" }, { "featureCode": "LINING", "boolean": true } ] } ``` Responses: `200`, `400`, `401`, `404`, `409` #### `GET /preview/items/{itemNumber}/configuration` — Get an item's configuration schema Returns everything needed to render a configurator form for one item: its configurable features, their input types, bounds, defaults and price impact, plus the allowed options for each selection. This is the first call in the configurator flow. Each feature's `featureType` decides which field to send back in `values[]`: `Text` → `text`, `Number` → `number` (bounded by `minValue`/`maxValue`), `Selection` → `optionCode`, `Boolean` → `boolean`, `ItemSelection` → `itemNumber`. `Calculated` features take no input — they are derived from the others and their results come back in `resolvedValues` from POST /preview/configurations/calculate. `features` is not paginated: a configurator cannot render a half-loaded form, so the whole schema always ships in one response. An item with no configurable features returns `isConfigurable: false` and an empty list — filter GET /preview/items with `?isConfigurable=true` to find the configurable ones. Prices here are the tenant base-currency list prices; use POST /preview/configurations/calculate for the real, customer-specific price of a given set of choices. Responses: `200`, `401`, `404` ### Suppliers Suppliers with their contact persons and supplier-specific prices, minimum order quantities and lead times. Addressed by supplierNumber. #### `GET /preview/suppliers` — List suppliers Returns a paginated list of suppliers for the authenticated tenant. Filter by ?search=, ?isActive= and ?modifiedSince= (ISO 8601 UTC datetime). ?search= performs a case-insensitive partial match against supplierNumber, name, organizationNumber and city. Example: ?search=acme matches suppliers whose name contains 'acme'. ?modifiedSince= returns both created and modified suppliers, so it can be used for delta sync. Sorted by supplierNumber ascending. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `search` (string), `isActive` (boolean), `modifiedSince` (date-time), `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401` #### `POST /preview/suppliers` — Create a supplier Creates a new supplier for the authenticated tenant. Only name is required. supplierNumber: if omitted a number is auto-generated from the tenant number sequence; if provided and already in use, 409 Conflict is returned. currencyCode defaults to the tenant base currency and determines the currency of purchase orders and supplier prices. paymentTermCode and deliveryTermCode must match existing reference data (400 if unknown) and become the defaults on purchase orders to this supplier. The response body is the same representation as GET /preview/suppliers/{supplierNumber}; the canonical URL is returned in the Location header and in links.self. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Minimal — name only: ```json { "name": "Nordic Components AB" } ``` Example request body — Full supplier: ```json { "name": "Nordic Components AB", "supplierNumber": "SUP-001", "organizationNumber": "5560001234", "currencyCode": "SEK", "purchaseOrderEmail": "order@nordic-components.se", "phone": "+46812345678", "street1": "Industrigatan 5", "postalCode": "41250", "city": "Göteborg", "countryCode": "SE", "paymentTermCode": "NET30", "deliveryTermCode": "DAP", "leadTimeDays": 14 } ``` Responses: `201`, `400`, `401`, `409` #### `GET /preview/suppliers/{supplierNumber}` — Get a supplier Returns a single supplier identified by its supplier number (exact match, case-sensitive). Returns 404 Supplier.NotFound if no supplier has that number. paymentTermCode and deliveryTermCode can be passed straight back when creating a purchase order. Responses: `200`, `401`, `404` #### `PATCH /preview/suppliers/{supplierNumber}` — Update a supplier Partially updates a supplier. Only provided fields are updated (JSON Merge Patch semantics). Omitted fields are left unchanged. Pass null to clear a nullable field. Unknown fields are silently ignored. supplierNumber cannot be changed — it is the resource's address. name and currencyCode cannot be set to null. Request body: `application/json`, `application/merge-patch+json` (required) Responses: `204`, `400`, `401`, `404` #### `GET /preview/suppliers/{supplierNumber}/contacts` — List supplier contacts Returns the contact persons registered on a supplier, default contact first, then by name. Returns 404 Supplier.NotFound if no supplier has that number. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401`, `404` #### `GET /preview/suppliers/{supplierNumber}/items` — List supplier prices Returns the supplier-specific purchase price, minimum order quantity, order multiple and lead time for each item this supplier can deliver. Prices are expressed in the supplier's currency (see currencyCode on the supplier). Filter by ?itemNumber= (exact match) to look up a single item, and ?isActive= to exclude retired price rows. validFrom/validTo bound the price period; a row with both null is always valid. Returns 404 Supplier.NotFound if no supplier has that number. Sorted by itemNumber ascending. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `itemNumber` (string), `isActive` (boolean), `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401`, `404` ### PurchaseOrders Purchase orders with lines, confirmation status and goods receipts. Addressed by orderNumber. Unlike sales orders, draft purchase orders are visible — an order created through this API starts in Draft. #### `GET /preview/purchase-orders` — List purchase orders Returns a paginated list of purchase orders for the authenticated tenant. Filter by ?status= (Draft, Sent, PartiallyConfirmed, Confirmed, PartiallyReceived, Received, Closed, Cancelled; matched case-insensitively, an unknown value returns 400), ?supplierNumber=, ?search=, ?orderDateFrom=, ?orderDateTo=, ?modifiedSince= (ISO 8601 UTC datetime). ?search= performs a case-insensitive partial match against both orderNumber and supplierReference. ?modifiedSince= returns both created and modified orders, so it can be used for delta sync. Unlike sales orders, Draft purchase orders ARE included — an order created via POST /preview/purchase-orders starts in Draft and the caller has to be able to find it again. Use ?status=Draft or ?status=Sent to narrow. Sorted by order date descending, then order number. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `status` (string; one of `Draft`, `Sent`, `PartiallyConfirmed`, `Confirmed`, `PartiallyReceived`, `Received`, `Closed`, `Cancelled`), `supplierNumber` (string), `orderDateFrom` (date), `orderDateTo` (date), `search` (string), `modifiedSince` (date-time), `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `400`, `401` #### `POST /preview/purchase-orders` — Create a purchase order Creates a new purchase order for the authenticated tenant. Only supplierNumber is required; all other fields default from the supplier or tenant settings. The order is created in Draft status and is NOT sent to the supplier — call POST /preview/purchase-orders/{orderNumber}/send when it is ready to go out. orderNumber: if omitted a number is auto-generated from the tenant number sequence; if provided and already in use, 409 Conflict is returned. Lines are created atomically with the order — if any line is rejected, no order is created. On a line, omit unitPrice to use the supplier price list, and omit unit to use the item's base unit (400 if the item has no base unit). The response body is the same representation as GET /preview/purchase-orders/{orderNumber}; the canonical URL is returned in the Location header and in links.self. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Minimal — supplier and one line: ```json { "supplierNumber": "SUP-001", "lines": [ { "itemNumber": "WIDGET-A", "quantity": "100" } ] } ``` Example request body — Full purchase order: ```json { "supplierNumber": "SUP-001", "orderDate": "2026-08-01", "expectedDeliveryDate": "2026-08-15", "warehouseCode": "MAIN", "currencyCode": "SEK", "paymentTermCode": "NET30", "deliveryTermCode": "DAP", "supplierReference": "OUR-REF-4711", "lines": [ { "itemNumber": "WIDGET-A", "quantity": "100", "unit": "st", "unitPrice": "42.50" }, { "itemNumber": "WIDGET-B", "quantity": "25", "expectedDate": "2026-08-22" } ] } ``` Responses: `201`, `400`, `401`, `409` #### `GET /preview/purchase-orders/{orderNumber}` — Get a purchase order Returns a single purchase order identified by its order number (exact match, case-sensitive), including all lines. Returns 404 PurchaseOrder.NotFound if no order has that number. Line numbers in the response are the business keys used by the line endpoints, e.g. PATCH /preview/purchase-orders/{orderNumber}/lines/{lineNumber}. confirmedQuantity, confirmedUnitPrice and confirmedDeliveryDate are null until the supplier has confirmed the line. Responses: `200`, `401`, `404` #### `PATCH /preview/purchase-orders/{orderNumber}` — Update a purchase order Partially updates a purchase order header. Only provided fields are updated (JSON Merge Patch semantics). Omitted fields are left unchanged. Pass null to clear a nullable field. Unknown fields are silently ignored. orderNumber and supplierNumber cannot be changed — the order number is the resource's address, and changing supplier would invalidate the prices on every line. orderDate, warehouseCode and currencyCode cannot be set to null. Reference codes that do not exist return 400 with the offending field named. Returns 409 Conflict if the order is cancelled or closed. To change lines, use the line endpoints. Request body: `application/json`, `application/merge-patch+json` (required) Responses: `204`, `400`, `401`, `404`, `409` #### `POST /preview/purchase-orders/{orderNumber}/cancel` — Cancel a purchase order Cancels a purchase order. Only orders where nothing has been received can be cancelled — use close instead when goods have already arrived but no more are expected. Returns 409 Conflict if the order is already received, closed or cancelled. Headers: `Idempotency-Key` (string; required) Responses: `204`, `401`, `404`, `409` #### `POST /preview/purchase-orders/{orderNumber}/close` — Close a purchase order Closes a purchase order so that no further receipts are expected, even if quantities remain outstanding. Use this to settle short deliveries the supplier will not complete. Returns 409 Conflict if the order is already closed or cancelled. Headers: `Idempotency-Key` (string; required) Responses: `204`, `401`, `404`, `409` #### `POST /preview/purchase-orders/{orderNumber}/confirm` — Confirm a purchase order Records that the supplier has confirmed the order, moving it to Confirmed. Pass supplierReference to store the supplier's own order number from their confirmation. The request body is optional. This is a header-level status transition only: it does not populate the per-line confirmation fields (confirmedQuantity, confirmedUnitPrice, confirmedDeliveryDate), which stay null and are reserved for the structured confirmation flow that is not yet part of the public API. If the supplier confirmed different quantities or dates, patch the affected lines (PATCH /preview/purchase-orders/{orderNumber}/lines/{lineNumber}) — note that this changes what is ordered, so the deviation is applied rather than recorded alongside the original values. Returns 409 Conflict unless the order is Sent or PartiallyConfirmed. Headers: `Idempotency-Key` (string; required) Request body: `application/json` Example request body — Confirm with the supplier's order number: ```json { "supplierReference": "SO-99887" } ``` Responses: `204`, `400`, `401`, `404`, `409` #### `GET /preview/purchase-orders/{orderNumber}/lines` — List purchase order lines Returns the lines on a purchase order, sorted by line number ascending. The same lines are also embedded in GET /preview/purchase-orders/{orderNumber}; this endpoint exists so that orders with many lines can be paged through. Returns 404 PurchaseOrder.NotFound if no order has that number. Response shape: { items: T[], totalCount: int, page: int, pageSize: int, totalPages: int, hasPreviousPage: bool, hasNextPage: bool }. Page is 1-based. Default pageSize is 50, maximum is 200. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `50`) Responses: `200`, `401`, `404` #### `POST /preview/purchase-orders/{orderNumber}/lines` — Add a line to a purchase order Adds a line to an existing purchase order. If unitPrice is omitted the price is resolved from the supplier price list for the item. If unit is omitted the item's base unit is used (400 if the item has no base unit). The Location header points at the new line's address, purchase-orders/{orderNumber}/lines/{lineNumber}. Returns 409 Conflict if the order is cancelled or closed, or otherwise in a state that does not permit new lines. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Add purchase order line: ```json { "itemNumber": "WIDGET-B", "quantity": "25", "unit": "st", "unitPrice": "17.90" } ``` Responses: `201`, `400`, `401`, `404`, `409` #### `PATCH /preview/purchase-orders/{orderNumber}/lines/{lineNumber}` — Update a purchase order line Partially updates a purchase order line identified by its line number. Only provided fields are updated (JSON Merge Patch semantics); omitted fields are left unchanged. quantity, unitPrice and unit cannot be null; pass null for expectedDate, promisedDeliveryDate or notes to clear them. Line and order totals are recalculated automatically. The item on a line cannot be changed — delete the line and add a new one instead. Returns 409 Conflict if the order is cancelled or closed, or if the quantity is reduced below what has already been received. Request body: `application/json`, `application/merge-patch+json` (required) Responses: `204`, `400`, `401`, `404`, `409` #### `DELETE /preview/purchase-orders/{orderNumber}/lines/{lineNumber}` — Delete a purchase order line Removes a line from a purchase order and recalculates the order totals. Returns 404 if the order or line number does not exist, and 409 Conflict if the line has already been received wholly or in part, or if the order is in a state that does not permit changes to its lines. Responses: `204`, `400`, `401`, `404`, `409` #### `POST /preview/purchase-orders/{orderNumber}/lines/{lineNumber}/receive` — Receive goods on a purchase order line Books received goods into stock against a purchase order line. This increases the on-hand quantity, creates an inventory transaction and advances the line and order status (PartiallyReceived, then Received once the full quantity has arrived). locationCode must be a location in the order's receiving warehouse; omit it to use that warehouse's default receiving location. unitCost records what the goods actually cost if it differs from the ordered price — it feeds the inventory valuation and must not be negative. serialNumber and batchNumber are stored on the receipt as supplied; they are not currently validated against the item's tracking type, so send the right one for the item. Receiving is not reversible through this API; a retry with the same Idempotency-Key replays the original response instead of booking the goods twice. Returns 409 Conflict if the order is in Draft or Cancelled, or if the quantity exceeds what is still outstanding on the line. The response contains the created receipt id and the line as it stands after the receipt. Headers: `Idempotency-Key` (string; required) Request body: `application/json` (required) Example request body — Receive the full outstanding quantity: ```json { "quantity": "100" } ``` Example request body — Partial receipt into a specific location with a batch number: ```json { "quantity": "40", "locationCode": "A-01-02", "unitCost": "42.75", "batchNumber": "B-2026-08-14", "notes": "Short delivery, remainder promised week 34" } ``` Responses: `200`, `400`, `401`, `404`, `409` #### `POST /preview/purchase-orders/{orderNumber}/send` — Send a purchase order Moves a purchase order from Draft to Sent. By default this only records that the order has gone out — nothing is emailed. That is the right behaviour when the order reaches the supplier over EDI or another channel you control. Set sendEmail=true to have Fluit email the purchase order PDF to the supplier; the address defaults to the supplier's purchaseOrderEmail and 400 is returned if neither that nor toEmail is set. Sending an email is not reversible, so retries with the same Idempotency-Key replay the original response instead of sending again. The request body is optional; POSTing with no body sends without email. Returns 409 Conflict if the order is cancelled, closed or has no lines. Headers: `Idempotency-Key` (string; required) Request body: `application/json` Example request body — Mark as sent without emailing (default): ```json {} ``` Example request body — Email the purchase order to the supplier: ```json { "sendEmail": true, "subject": "Purchase order PO-2026-00042" } ``` Responses: `204`, `400`, `401`, `404`, `409` ### Reference Read-only reference data: the valid codes for warehouses, currencies, payment/delivery terms, order types and shipping methods used in other requests. #### `GET /preview/reference/currencies` — List currencies Returns all configured currencies. Use the code field as currencyCode in order creation. Reference lists are bounded configuration data and are returned unpaginated. Responses: `200`, `401` #### `GET /preview/reference/delivery-terms` — List delivery terms Returns all delivery terms available for use as deliveryTermCode in order creation. Reference lists are bounded configuration data and are returned unpaginated. Responses: `200`, `401` #### `GET /preview/reference/order-types` — List order types Returns all order types available for use as orderTypeCode in order creation. Reference lists are bounded configuration data and are returned unpaginated. Responses: `200`, `401` #### `GET /preview/reference/payment-terms` — List payment terms Returns all payment terms available for use as paymentTermCode in order creation. Reference lists are bounded configuration data and are returned unpaginated. Responses: `200`, `401` #### `GET /preview/reference/shipping-methods` — List shipping methods Returns shipping methods that have a code and can be referenced via shippingMethodCode in order creation. Reference lists are bounded configuration data and are returned unpaginated. Responses: `200`, `401` #### `GET /preview/reference/tenant-info` — Get tenant info Returns company details and base configuration (currency, language, timezone) for the authenticated tenant. Address fields use the same names as elsewhere in the API (street1, countryCode) and codes are ISO: countryCode is ISO 3166-1 alpha-2, baseCurrencyCode is ISO 4217 and defaultLanguageCode is ISO 639-1. Responses: `200`, `401` #### `GET /preview/reference/warehouses` — List warehouses Returns all active warehouses available for use as warehouseCode in order creation. Reference lists are bounded configuration data and are returned unpaginated. Responses: `200`, `401`