Hoppa till innehållet

API-referens

Hela det publika REST-API:et: 134 anrop i 17 grupper och 218 scheman. Sidorna genereras ur API:ets OpenAPI-dokument och uppdateras automatiskt efter varje driftsättning.

Bas-URL https://api.erp.fluit.cloud/preview Autentisering X-Api-Key swagger.json llms.txt

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.

Endpoints

Kunder 6

Customers with their contact persons and delivery addresses. Addressed by customerNumber.

GET POST PATCH
Artiklar 10

Items/products including stock availability, price calculation, units, attributes and assets. Addressed by itemNumber.

GET POST PATCH
Kundorder 9

Sales orders with lines, fulfilment status and shipments. Addressed by orderNumber. Draft orders are not visible.

GET POST PATCH DELETE
Konfigurator 8

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 POST DELETE
Leverantörer 6

Suppliers with their contact persons and supplier-specific prices, minimum order quantities and lead times. Addressed by supplierNumber.

GET POST PATCH
Inköpsorder 13

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 POST PATCH DELETE
Ärenden 20

Support tickets through their whole handling flow: submit one from a contact form, follow its status, update it, exchange messages with the reporter, route it to a team's queue and take it through triage, work, resolution and closure. Addressed by ticketNumber. Tickets created here land in the same queue as tickets created inside the ERP and from the support mailbox. Assignment to a named agent is not exposed — agents are ERP users, and this API routes work by queue instead. Queue codes come from GET /preview/reference/ticket-queues.

GET POST PATCH DELETE
Leveranser 12

Shipments: the physical fulfilment of sales orders, from warehouse release through picking, packing and carrier booking to delivery. Addressed by shipmentNumber. A shipment can cover several orders (consolidation) and an order can have several shipments (partial delivery), so shipments are their own resource rather than a sub-resource of the order.

GET POST
Fakturor 2

Sales invoices with their lines and totals. Addressed by invoiceNumber.

GET
Offerter 8

Sales quotes with lines and validity. Addressed by quoteNumber. A quote that is accepted or explicitly converted becomes a sales order.

GET POST
Returer 6

Customer returns (RMA): register a return against a sales order, approve it and receive the goods back into stock. Addressed by returnNumber.

GET POST
Lager 11

Stock on hand, the stock ledger and the operations that move it: adjustments, relocations and physical counts. Availability per item lives under Items; this tag covers what has happened and how to change it.

POST GET
Överföringar 7

Stock transfers between warehouses, from release through shipping to receipt. Addressed by orderNumber.

GET POST
Inleveranser 3

Goods receipts — what physically arrived, from purchase orders and inbound transfers. Addressed by receiptNumber. Receiving against a purchase order line is done under PurchaseOrders.

GET
Arbetsordrar 2

Production and service work orders. Read-only. Addressed by workOrderNumber.

GET
Synkronisering 1

Endpoints that exist for incremental synchronisation. The list endpoints' ?modifiedSince= covers records that were created or changed; GET /preview/deletions covers the ones that were removed, which no list endpoint can report because the row is gone.

GET
Referensdata 10

Read-only reference data: the valid codes for warehouses, currencies, payment/delivery terms, order types and shipping methods used in other requests.

GET

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:
curl https://api.erp.fluit.cloud/preview/items?pageSize=5 \
  -H "X-Api-Key: fluit_live_sk_..."

Scopes

A key carries a space-separated list of scopes, and every operation requires one. The scope is {resource}:read for reads and {resource}:write for writes, where the resource is the operation's tag in lower kebab-case — items:read, sales-orders:write, inventory:write. Each operation states its scope in the description and in the x-required-scope extension.

Give a key only what its integration needs. A webshop that reads the catalogue and places orders wants items:read sales-orders:write, and nothing more — with that list it cannot write off stock or post an inventory count, even though those endpoints exist on the same API. Use * for a key that should reach everything.

Calling an operation the key lacks the scope for returns 403 with the required and granted scopes in the problem details.

Keys created before scopes were enforced have an empty scope list. Those keep working with unrestricted access, which is the access they already had. Set scopes on the key to narrow it — there is no way to widen an empty list, because it is already unlimited.

  1. Create your first order (note the required Idempotency-Key on POST):
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
    Tickets ticketNumber

    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. Invariant means . as the decimal separator and no thousands separator — "1234.50". A value in any other format ("1 234,50", "1,234.50") is rejected with 400, never guessed at: "1,5" could as easily mean 15 as 1.5, and a quantity is not something to get approximately right. Format for the wire, not for a reader — toFixed(2) in JavaScript, str(Decimal(...)) in Python, ToString(CultureInfo.InvariantCulture) in .NET. The same rule applies to decimal query parameters such as ?quantity=. 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 and carry the Z designator (2026-06-09T22:07:47.7639194Z), so new Date(...) in JavaScript and datetime.fromisoformat(...) in Python both resolve them to the right instant without any correction on your side. Request parameters are equally forgiving: 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, Released, Closed, …) 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 that has its own URL carries links.self, the canonical URL of that resource. POST responses return the same representation as the corresponding GET, with the URL in the Location header as well. Rows that are only ever read through their parent — order lines, contacts, addresses, packages, reference data — have no self link, because they have no address of their own.

  • 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. A field that is not part of the endpoint's contract is rejected with 400, naming the field and listing the ones it accepts — so a misspelling fails loudly instead of looking like a change that did not take. The two line endpoints (PATCH /preview/orders/{orderNumber}/lines/{lineNumber} and its purchase-order counterpart) ignore unknown fields instead; their descriptions say so.

  • 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 has its own endpoints: GET /preview/inventory/availability covers many items in one call — pass up to 200 item numbers in ?itemNumbers=, or page through the whole stocked catalogue — and GET /preview/items/{itemNumber}/availability covers one. Use the bulk endpoint for a catalogue sync, with ?modifiedSince= so a recurring poll only pays for what moved; one call per item costs 1 + N against the per-minute quota. Remember that 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.

Support: take in a ticket from your own form

  1. POST /preview/tickets with title, description and — if you have it — the reporter's contactEmail and customerNumber. That is the whole contract; the endpoint is meant to sit behind a contact form on your own site.
  2. The reporter gets a confirmation mail with the ticket number, and your agents see the ticket in the same queue as tickets phoned in or mailed to the support mailbox.
  3. GET /preview/tickets/{ticketNumber} reads back the current status, so a "track my ticket" page can show the reporter where their case stands.

Answers are written by your agents in Fluit and reach the reporter by email; replies to that mail land back on the same ticket. This API is the way in, not a chat channel.

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 — Numbernumber, SelectionoptionCode, Booleanboolean, ItemSelectionitemNumber, Texttext. 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:

{ "items": [], "totalCount": 0, "page": 1, "pageSize": 50,
  "totalPages": 0, "hasPreviousPage": false, "hasNextPage": false }

Incremental sync

A sync needs three things: what was created, what changed, and what was removed. Creates and changes come from the list endpoints; removals need a separate feed, because a deleted record leaves nothing behind for a list endpoint to return.

Upserts — ?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. A change anywhere inside the record counts: editing an order line moves the order's modifiedDate, so nothing can change below the level of the resource you are polling without the resource itself showing up in the delta.

Removals — GET /preview/deletions?deletedSince=. Deletions cannot be observed from a list endpoint. A deleted customer, supplier, purchase order or configuration is really gone, so it simply stops appearing — which is indistinguishable from "unchanged since your last poll". Every deletion is instead recorded in a log, written in the same transaction as the deletion itself, and read from this endpoint. Each entry carries the resource, the id and the businessKey the record had when it was deleted, so you can match it against your copy.

A sync run therefore looks like:

since = <timestamp stored at the start of the previous run>
now   = <timestamp now, stored for the next run>

GET /preview/customers?modifiedSince={since}        # and the other collections you mirror
GET /preview/deletions?deletedSince={since}         # what to retire

Both directions are safe to re-read from a slightly earlier timestamp: applying the same upsert or the same deletion twice has no further effect. Prefer overlapping a little over cutting it fine.

Conditional reads — ETag / If-None-Match. GET on a single record returns a weak ETag. Pass it back in If-None-Match and you get 304 Not Modified with an empty body while the record is unchanged. The validator follows the whole record, nested parts included, so a 304 is a real promise that nothing in the representation has moved.

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

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