# Fluit Headless Commerce API > The commerce API behind Fluit's own storefront, available for your own front end. > Catalogue, search, pricing, cart, checkout and the signed-in customer's pages — the same > endpoints our storefront calls, reading the same ERP that runs the warehouse and the > ledger. There is no separate commerce database to keep in sync: stock is the stock, the > price is the price a salesperson would quote, and an order placed here is an order in > Fluit. - Base URL: `https://api.erp.fluit.cloud` — the paths listed below already include the `/ecom` prefix. - Authentication: send the tenant in the `X-Tenant-Id` header and the storefront in `X-Channel` on every request, plus a session token from GET /ecom/session as `Authorization: Bearer` on everything that concerns a visitor. - Version: `v1`. - OpenAPI document, with the complete request and response schemas: https://api.erp.fluit.cloud/swagger/ecom/swagger.json - Reference documentation for humans: https://fluit.se/api-docs/commerce-reference - Contact: info@fluit.se ## Getting started Three calls get you from a domain name to a product listing. **1. Resolve the domain to a tenant and a channel.** This is the only call that needs no headers — it runs above tenant context and exists to establish it. ```bash curl "https://api.erp.fluit.cloud/ecom/session/resolve?domain=shop.acme.com" # { "tenantId": "…", "channelCode": "web", "channelName": "Acme Web" } ``` You can skip this call and configure the tenant id and channel code directly if you know them. It exists for the multi-domain case, where one deployment serves several storefronts and the domain decides which. **2. Open a session.** The response carries the token you send from here on, plus the channel's full configuration: currency, language, feature flags, branding, navigation and SEO settings. ```bash curl "https://api.erp.fluit.cloud/ecom/session" \ -H "X-Tenant-Id: $FLUIT_TENANT_ID" \ -H "X-Channel: web" # { "token": "…", "expiresAt": "…", "isAuthenticated": false, "cartItemCount": 0, "channel": { … } } ``` **3. Call everything else** with the tenant, the channel and the session token. ```bash curl "https://api.erp.fluit.cloud/ecom/catalog/products?pageSize=20" \ -H "X-Tenant-Id: $FLUIT_TENANT_ID" \ -H "X-Channel: web" \ -H "Authorization: Bearer $SESSION_TOKEN" ``` ## Tenants and channels Two headers scope every request. `X-Tenant-Id` selects the company. It is a GUID, and it is not a secret: this surface only ever returns what the channel has published, so knowing the id gets you the same catalogue a visitor sees in the browser. Everything that is not public — cost prices, other customers, the ledger — lives behind endpoints this API does not have. `X-Channel` selects the storefront within that company. A channel owns its assortment, price list, currency, language, VAT display and branding, so the same item can be published in two channels at different prices under different names. **Omitting the header falls back to the first active channel**, which is convenient in a single-channel tenant and a silent source of wrong prices in a multi-channel one. Send it explicitly. Channel codes come from `GET /ecom/channels`, or from the domain resolution above. ## Sessions `Authorization: Bearer` carries an `EcomSession` token. It is an opaque handle, not a JWT — do not try to decode it, and do not expect claims inside it. A session is not a login. `GET /ecom/session` issues one to an anonymous visitor, and that anonymous session carries a cart. Signing in through `POST /ecom/auth/login` or the one-time-code endpoints upgrades the session in place, so the cart survives the login and prices switch to the customer's agreement prices in the same moment. `isAuthenticated` on the session response tells you which state you are in. Anonymous is not the same as tokenless. The cart belongs to the session, so every cart, checkout and customer-portal call needs that token even before anyone has signed in — without it they answer `401`. The sign-in endpoints need one too, because signing in upgrades a session that must already exist. Get the token first, then use it throughout. Three groups work without a token: catalogue and content read the same for everyone, and `GET /ecom/session/resolve` runs before there is a session to have. Two more take one when offered and answer anyway without it: `GET /ecom/catalog/prices` and `GET /ecom/catalog/stock` fall back to list prices and channel-level stock. Each operation's `security` says which case it is. Sessions live for seven days and extend themselves as they are used. Requests from crawlers are recognised by user agent and served without persisting a session, so indexing a catalogue does not fill the session table. ## Architecture: calling from your own server The intended shape is server to server. Your front end calls your own backend, and your backend calls Fluit — holding the tenant id, the session token and any customer credentials on your side, and exposing to the browser only what that page needs. This is how our own storefront is built. It is a SvelteKit app whose pages load through server routes, with a thin set of proxy endpoints under its own origin for the calls that have to happen after hydration. The browser never talks to this API directly. That shape also decides the CORS answer: browser requests come from your origin, which is allow-listed per tenant in configuration rather than open to the world. Ask us to add a domain if you need direct browser calls. Server-side calls have no such restriction. Two practical consequences of proxying: - Forward the visitor's address in `X-Forwarded-For`. The rate limiter partitions on it, and without it every visitor shares your server's quota. - Cache what does not change per visitor — but you do not have to work out which is which. Every response says so itself in `Cache-Control`. See *Caching* below. ## Catalog and search Products are addressed by slug or id — `GET /ecom/catalog/products/{idOrSlug}` accepts either, so a URL can carry the readable one. Lists take `category`, `search`, `sort`, `page` and `pageSize`, plus attribute filters as `attr_{code}=value1,value2`. `GET /ecom/catalog/filters` returns the facets available for a given category or search, with counts, so the filter panel reflects what is actually in the result rather than the full attribute vocabulary. `GET /ecom/catalog/suggest` powers type-ahead. Search is index-backed with relevance ranking, synonyms and typo tolerance — not a substring match — so results are ordered by relevance unless you pass an explicit `sort`. ## Prices, stock and VAT Prices come from the channel's price list, and from the customer's agreement prices when the session is signed in. The same product therefore has no single price: it has the price for this channel and this visitor. Fetch prices for a set of items in one call with `GET /ecom/catalog/prices?itemIds=…` rather than reading them off cached product payloads. Whether amounts include VAT is a channel setting, and some channels let the visitor toggle it. Read `channel.features.showPricesIncludingVat` and `allowCustomerVatToggle` from the session response and render accordingly — the numbers on the wire follow the channel, and a front end that assumes one convention will be wrong on the other. Stock is available separately through `GET /ecom/catalog/stock?itemIds=…`, aggregated according to the channel's `stockAggregation` setting. `showStock` and `showOutOfStockProducts` decide whether a storefront is supposed to display it at all. Both endpoints take at most **100 ids per request**, and both have a `POST` variant that takes the same ids in a JSON body: ```bash curl -X POST "https://api.erp.fluit.cloud/ecom/catalog/prices" \ -H "X-Tenant-Id: $FLUIT_TENANT_ID" -H "X-Channel: web" \ -H "Content-Type: application/json" \ -d '{ "itemIds": ["…", "…"] }' ``` The `POST` exists because of URL length, not because of the limit: with GUIDs a typical URL budget runs out around 50 ids, well before the 100 the server actually allows. Same handler, same response body, so you can switch without touching your parser. The limit is the same on both — it protects response time, since the price engine runs per item — so chunk into batches of 100 either way. One difference worth knowing: the `GET` silently skips ids it cannot parse, while a malformed id in the JSON array fails the whole request with `400`. Neither variant is cacheable. Both answer `no-store`, because the price depends on the signed-in customer's agreement. ## From cart to order The cart hangs off the session, so there is no cart id to carry: 1. `POST /ecom/cart/items` with an item id and a quantity. 2. `GET /ecom/checkout/data` for the shipping and payment methods this channel offers, plus the known customer details when signed in. 3. `POST /ecom/checkout/sessions` to start payment with the channel's provider. 4. `POST /ecom/checkout/place` to place the order. 5. `GET /ecom/checkout/sessions/{sessionId}/confirmation` on the return page. Step 3 is provider-agnostic. The response carries a `renderMode` and exactly one of `htmlSnippet`, `redirectUrl` or `clientSecret`, and your front end acts on the mode rather than on the provider's name. That is what lets a tenant change payment provider without a front-end release. `POST /ecom/checkout/apply-code` takes both discount coupons and gift cards; the response says which it was and what it did to the total. > The endpoints under `/ecom/kco/*` are the Klarna-specific predecessor of the same flow. > They still work and our own storefront still uses them, but new builds should use > `/ecom/checkout/sessions`. The KCO endpoints will not gain features. ## Payment provider callbacks `POST /ecom/checkout/webhooks/{provider}` and `POST /ecom/kco/push` are **inbound**. The payment provider calls them when a payment settles; you never do. They are documented because you may need to configure their URLs in the provider's dashboard, and because seeing them here explains how an order can change state without your front end doing anything. ## Customer portal Everything under `/ecom/portal/*` is the signed-in customer's own record: orders, invoices, shipments, quote requests, returns, support tickets, addresses and profile. All of it requires a session that has been authenticated, and all of it is scoped to that customer — there is no way to read another customer's data through these endpoints. This is not the same thing as Fluit's partner portal, which lives under `/portal` and has its own API. The names are close; the surfaces are unrelated. ## Idempotency A timeout on `POST /ecom/checkout/place` is the one failure that a storefront cannot reason its way out of on its own: the order may or may not exist, and asking again without protection either places a second one or answers that the cart is already converted — which tells you the order exists but not what it was called. Send an `Idempotency-Key` header to close that gap. Use a unique value per logical attempt, a UUID is the obvious choice, and reuse the *same* value on every retry of that attempt: ```bash curl -X POST "https://api.erp.fluit.cloud/ecom/checkout/place" \ -H "X-Tenant-Id: $FLUIT_TENANT_ID" \ -H "X-Channel: web" \ -H "Authorization: Bearer $SESSION_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ … }' ``` The first call runs normally. A retry with the same key replays the original response — the same status, the same body and the same `Location` — with `Idempotency-Replayed: true` added, and without the handler running again. Other response headers are not replayed, so read the outcome from the body rather than from them. Keys are scoped to the session and kept for 24 hours. **The header is optional here.** That is a deliberate difference from the Fluit Public API, where it is required on every POST: this surface already has clients, and making it mandatory would have broken all of them at once. Omit it and the call behaves exactly as it did before. Two responses exist only when you send the header: - `422` — the key was already used for a *different* request, meaning another endpoint or the same endpoint with a different body. Use a new key for new requests; reuse a key only when retrying the same one. - `503` with `Retry-After` — a request with the same key is still in flight. Retry with the same key once it finishes. A `5xx` is never replayed. Server errors are not a final answer to your request, so the key is released and a retry genuinely runs the operation again. Honoured on `POST /ecom/checkout/place` and `POST /ecom/checkout/sessions`. **`POST /ecom/cart/items` deliberately does not honour it**, and that is not an oversight: adding the same item twice is a thing shoppers legitimately do, and suppressing the second add would silently drop a real one. ## Pagination List endpoints return a fixed envelope: ```json { "items": [], "totalCount": 0, "page": 1, "pageSize": 20, "totalPages": 0, "hasPreviousPage": false, "hasNextPage": false } ``` `page` is 1-based. Page sizes are clamped per endpoint; ask for more than the maximum and you get the maximum, not an error. ## Caching Every response carries a `Cache-Control` header, and following it is better than inventing your own TTLs. There are two kinds. **Channel-wide responses** — the catalogue, search, categories, brands, filters, content pages, widgets and the redirect table — answer `public, max-age=…` with an `ETag`: | Response | max-age | | --- | --- | | Category tree | 3600 | | Product detail | 600 | | Product feed | 3600 | | Product lists, search, filters, brands | 300 | | Content pages, widgets, redirects | 300 | | Type-ahead suggestions | 300 | Those numbers are not advice — they are the same TTLs the API uses for its own internal cache. Honouring them therefore adds no staleness that we do not already have. Revalidation is cheap: send the `ETag` back as `If-None-Match` and an unchanged response answers `304` with no body. ```bash curl -H "If-None-Match: $ETAG" \ -H "X-Tenant-Id: $FLUIT_TENANT_ID" -H "X-Channel: web" \ "https://api.erp.fluit.cloud/ecom/catalog/categories" ``` **Everything else answers `no-store`** and carries no `ETag`. That is the default for the whole surface, not a list we maintain: the cart, checkout, the customer portal, prices and stock all fall under it, and so does any endpoint we add tomorrow. Prices in particular depend on the signed-in customer's agreement, so there is no shared version of them to keep. Caching those per authenticated customer inside your own layer is fine — that is a distinction only you can draw. > **If you put a shared cache or CDN in front of this API, you must vary on > `X-Tenant-Id` and `X-Channel`.** Both are headers, neither appears in the URL, and both > decide what the response contains. We send `Vary: X-Tenant-Id, X-Channel` on every > cacheable response for exactly this reason — a cache keyed on the URL alone would serve > one tenant's catalogue to another. Images and documents under `/ecom/catalog/assets/*` are immutable for practical purposes and answer `public, max-age=86400` and `3600` respectively. ## Errors | Status | Means | | --- | --- | | `400` | The request is malformed, or a value is invalid | | `401` | The endpoint needs a session and none was sent, or the token has expired | | `403` | The session exists but is not allowed to see this record | | `404` | No such product, page, channel or record in this channel | | `409` | The record is not in a state where this makes sense — a cancelled order, a used coupon | | `429` | Rate limit — see below | **Do not assume one body shape.** This surface predates the reference and carries three, and a client that parses every failure as RFC 7807 will throw on two of them: - Most `400`, `403`, `404`, `409` and `500` responses are `application/problem+json` per RFC 7807, with `type`, `title`, `status` and `detail`, plus an `errors` object on validation failures. - Some endpoints — mainly the channel-`404` on catalogue, content and blog reads, and the `400` on the id-list endpoints — answer `application/json` with a flat `{ "error": "…" }` instead. Each operation's documented response schema is the truth; where it says `ProblemDetails` you get RFC 7807, otherwise expect the flat shape. - `401` has **no body at all**. The status code is the whole message. - `429` is `application/json` with problem-like fields, but not the problem media type. Branch on the status code and the `Content-Type`, not on the assumption. Consolidating these onto one shape would break clients that read the current one, so it will happen as an announced change rather than quietly. A `404` from a catalogue endpoint usually means "not published in this channel" rather than "does not exist". That distinction is deliberate: an unpublished product should be indistinguishable from a missing one. ## Rate limiting Every endpoint is rate limited. Quotas are per tenant and channel, and then per visitor — by client IP for anonymous traffic and by session token once there is one — so one busy visitor cannot spend the channel's budget. | Traffic | Limit | | --- | --- | | Catalogue and search | 300 / minute per IP | | Session | 60 / minute per IP anonymous, 120 with a token, 600 for recognised crawlers | | Cart | 60 / minute | | Checkout | 10 / minute | | Sign-in | 5 / minute per IP | | Request a login code | 3 / 15 minutes per IP | | Verify a login code | 10 / minute per IP | | Help articles | 100 / minute per channel | | Page-view tracking | 300 / minute per channel | | Shopping assistant | 20 / minute | | Submit a review | 5 / hour per IP | | Newsletter sign-up | 10 / hour per IP | | Newsletter unsubscribe | 20 / hour per token | **There are no `X-RateLimit-*` headers on this API.** You discover the quota by hitting it: a rejected request returns `429` with `Retry-After` in seconds and a problem-shaped `application/json` body. Honour `Retry-After` rather than retrying on a fixed delay. The per-IP partitions depend on `X-Forwarded-For` reaching us. See *Architecture* above. ## Status and stability This is the API behind our own storefront, and it moves with it. Additive changes — new endpoints, new optional fields, new enum members — happen without notice, so read defensively and ignore fields you do not recognise. Changes that break an existing contract are announced in the Fluit changelog before they ship. It is a different promise from the Fluit Public API, which is versioned for third-party integrations. If you are synchronising an external system rather than building a storefront, that is the surface you want. ## Endpoints 94 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. ### Ecom.Session Establishing context: map a domain to a tenant and channel, then open a session. The session response also carries the channel's whole configuration — currency, language, feature flags, branding, navigation and SEO — so a storefront can render its shell from one call. #### `GET /ecom/session` — Get or create session Returns a session token and full channel context including branding, features, and analytics. Creates an EcomSession entity on first visit and reuses it on subsequent visits. Headers: `X-Channel` (string) Responses: `200`, `404`, `429` #### `GET /ecom/session/resolve` — Resolve domain to tenant Maps a domain (e.g., shop.acme.com) to tenant and channel IDs. Returns routing info only — full channel data comes from GET /ecom/session. Query parameters: `domain` (string), `channel` (string) Responses: `200`, `404`, `429` ### Ecom.Channels The storefronts a tenant runs, and the home page each one presents. A channel owns its assortment, price list, currency and branding, so the same item can be published in two channels at different prices. #### `PATCH /ecom/branding` — Update channel custom CSS Updates the custom CSS for the current channel's branding. Requires the X-Channel header and a valid X-Admin-Preview token for the same tenant and channel. The CSS is sanitised before it is stored, so the value in the response is what was actually saved and may differ from what was sent. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `404`, `429` #### `POST /ecom/branding/ai-suggest` — Suggest branding CSS for an element Proposes custom CSS for one element of the storefront, given the channel's current branding and a description of what to change, and streams the suggestion as server-sent events. It only proposes — PATCH /ecom/branding is what stores anything. Part of the in-store admin overlay and gated by the same admin preview token, so it is not an endpoint a storefront calls on a visitor's behalf. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `429` #### `GET /ecom/channels` — List available channels Returns a list of available channels from DomainMappings. No authentication or tenant context required. Headers: `X-Channel` (string) Responses: `200`, `429` #### `GET /ecom/homepage` — Get home page configuration Returns the channel's configured home page layout and visible sections, filtered by date for banners. Headers: `X-Channel` (string) Responses: `200`, `404`, `429` ### Ecom.Catalog Products, categories, brands, search, facets and type-ahead. Products are addressed by slug or id, and only what is published in the current channel is visible. Includes the product feed for Google Merchant and the binary endpoints that serve images and documents. #### `GET /ecom/assets/{tenantId}/{id}` — Get product image Serves a public product image. No authentication required. Tenant resolved from URL. Pass ?w= with one of the supported widths to get a resized WebP variant for responsive srcset. Query parameters: `w` (integer) Headers: `X-Channel` (string) Responses: `200`, `400`, `404`, `429` #### `GET /ecom/assets/{tenantId}/{id}/download` — Get product document Serves a public product document (datasheet, manual, firmware). No authentication required. Tenant resolved from URL. PDFs are served inline, everything else as a download. Headers: `X-Channel` (string) Responses: `200`, `404`, `429` #### `GET /ecom/catalog/brands` — Get brands (suppliers) by id Returns display data for the suppliers referenced by a Brands section. Query parameters: `ids` (string) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `429` #### `GET /ecom/catalog/categories` — Get category tree Returns the hierarchical category tree for navigation. Query parameters: `parentId` (string), `ids` (string) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `429` #### `GET /ecom/catalog/filters` — Get catalog filters Returns filterable attributes with available values and product counts. Counts are scoped to the channel, the optional category and the optional search term. Query parameters: `category` (string), `search` (string) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `429` #### `GET /ecom/catalog/list-widgets` — Get product list page widgets Returns active ProductList_Banner and ProductList_Sidebar widgets matching the current channel and optional category. Query parameters: `categoryCode` (string) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `404`, `429` #### `GET /ecom/catalog/product-feed` — Product feed rows Returns feed-ready product rows (identifiers, prices, availability, Google product category) for building Google Shopping and Meta catalog feeds. Page size is clamped to 1-1000. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `500`) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `429` #### `GET /ecom/catalog/products` — List products Returns a paginated list of products with optional filtering by category and search term. Pass ids=guid,guid to fetch a hand-picked set; without an explicit sort they are returned in the order given. Only includes products active on the current channel. Query parameters: `category` (string), `search` (string), `page` (integer; default `1`), `pageSize` (integer; default `20`), `sort` (string), `ids` (string) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `429` #### `GET /ecom/catalog/products/{idOrSlug}` — Get product details Returns complete product information including stock status, images, and attributes. Only shows products active on the current channel. Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `404`, `429` #### `GET /ecom/catalog/related-products` — Get related products by type Returns up to 8 unique related product summaries for the given item IDs and relation type. Types: Related, Accessory, CrossSell, UpSell, SparePart. Query parameters: `itemIds` (string; required), `type` (required) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `400`, `429` #### `GET /ecom/catalog/search` — Search products Full-text product search with relevance ranking, synonym expansion, typo tolerance and zero-result fallbacks. Query parameters: `q` (string), `category` (string), `page` (integer; default `1`), `pageSize` (integer; default `24`), `sort` (string) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `429` #### `GET /ecom/catalog/stock` — Get stock status for items Returns aggregated stock status for a batch of items. Returns nothing when the storefront hides stock, or when it hides stock from visitors who are not logged in and the request carries no customer session. Maximum 100 item ids per request; ids that cannot be parsed are skipped. With GUIDs the URL runs out before that limit does — use POST /ecom/catalog/stock for longer lists. Query parameters: `itemIds` (string; required) Headers: `X-Channel` (string) Responses: `200`, `400`, `404`, `429` #### `POST /ecom/catalog/stock` — Get stock status for items (ids in body) Identical to GET /ecom/catalog/stock — same handler, same response body — but takes the item ids in a JSON body, for id lists long enough to strain the URL. Both variants enforce the same limit of 100 ids per request. Returns nothing when the storefront hides stock, or when it hides stock from visitors who are not logged in and the request carries no customer session. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `404`, `429` #### `GET /ecom/catalog/suggest` — Search suggestions Type-ahead suggestions for the storefront search field: matching products, term completions and categories. Query parameters: `q` (string), `limit` (integer; default `6`) Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `429` #### `GET /ecom/products/{itemId}/widgets` — Get matching product widgets for a product Returns active HTML widgets that match the given product based on category, supplier, item group, price range, etc. Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `404`, `429` ### Ecom.Pricing Prices for a set of items in one call. The answer depends on the channel and on whether the session is signed in, because a signed-in customer gets their agreement prices. Do not cache the result across visitors. #### `GET /ecom/catalog/prices` — Get prices for items Returns calculated prices for a batch of items. Supports customer-specific pricing for authenticated B2B sessions via the session token. Respects channel price lists, campaigns, and quantity breaks. Maximum 100 item ids per request; ids that cannot be parsed are skipped. With GUIDs the URL runs out before that limit does — use POST /ecom/catalog/prices for longer lists. Query parameters: `itemIds` (string; required) Headers: `X-Channel` (string) Responses: `200`, `400`, `404`, `429` #### `POST /ecom/catalog/prices` — Get prices for items (ids in body) Identical to GET /ecom/catalog/prices — same handler, same response body — but takes the item ids in a JSON body. Use it when the id list is long enough to strain the URL: with GUIDs a typical URL budget runs out around 50 ids, well below the limit of 100 that both variants enforce. Respects channel price lists, campaigns and quantity breaks, and returns the customer's agreement prices when the session is signed in. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `404`, `429` ### Ecom.Redirects The channel's URL redirect table, for a storefront that handles its own routing. Read once and cache — it is channel-wide and changes rarely. #### `GET /ecom/redirects` — Get active URL redirects for the current channel Returns the channel's whole active redirect table in one call: source path, target path and the HTTP status to answer with (301 or 302). Intended to be read once and held in memory by the storefront, not called per request — the table is channel-wide and changes only when someone edits it in Fluit. Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `404`, `429` ### Ecom.ContentPages CMS pages: the navigable list and a page by slug, with its sections. Used for terms, about, delivery information and anything else the tenant edits without a deploy. #### `GET /ecom/pages` — Get published content pages for navigation Returns every published page in the channel as a flat list, in the tenant's own sort order. Each entry carries parentPageId and showInNavigation, so a storefront can build a nested menu from one call and still know which pages exist without appearing in it. Read the page body itself with GET /ecom/pages/{slug}. Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `404`, `429` #### `GET /ecom/pages/{slug}` — Get a published content page by slug Returns a published page with its SEO fields and its sections in display order. Sections are typed — text, image, testimonial, feature cards — and a storefront renders each kind its own way, so treat an unfamiliar section type as something to skip rather than fail on. An unpublished or missing page both return 404; the two are deliberately indistinguishable. Headers: `X-Channel` (string), `If-None-Match` (string) Responses: `200`, `304`, `404`, `429` ### Ecom.Blog Blog and news posts for the channel, with tag filtering and related-post lookup. #### `GET /ecom/blog` — List published blog posts / news Returns the channel's published posts, newest first, with their tags and cover images. Filter with ?tag= and choose the kind with ?type=BlogPost (default) or ?type=NewsPost; an unrecognised type falls back to BlogPost. Page is 1-based, default pageSize is 12 and the maximum is 50. Scheduled posts appear on their publish date, so the response is time-dependent and cached for a short while. Query parameters: `page` (integer), `pageSize` (integer), `tag` (string), `type` (string) Headers: `X-Channel` (string) Responses: `200`, `404`, `429` #### `GET /ecom/blog/related` — Get related articles for an article Returns other published posts related to the one identified by ?slug=, ranked by how many tags they share with it and falling back to the most recent when nothing overlaps. Candidates are of the same kind as the source post, so a news post never suggests a blog post. Default ?take= is 3 and the maximum is 12. The article itself is never included, and an unknown slug returns an empty list rather than 404. Query parameters: `slug` (string; required), `take` (integer) Headers: `X-Channel` (string) Responses: `200`, `404`, `429` ### Ecom.Help Knowledge base articles: browse by category, read by slug, search, and look up the articles attached to a specific product. #### `GET /ecom/help/articles` — List published FAQ / knowledge articles Returns published knowledge articles grouped by category, with locale-aware translations. Query parameters: `category` (string), `locale` (string) Headers: `X-Channel` (string) Responses: `200`, `429` #### `GET /ecom/help/articles/by-item/{itemId}` — Get knowledge articles for a specific item Returns published knowledge articles linked to the given item ID. Query parameters: `locale` (string) Headers: `X-Channel` (string) Responses: `200`, `429` #### `GET /ecom/help/articles/{slug}` — Get a knowledge article by slug Returns a single published knowledge article with full Markdown content. Query parameters: `locale` (string) Headers: `X-Channel` (string) Responses: `200`, `404`, `429` #### `GET /ecom/help/search` — Search knowledge articles Full-text search across published knowledge articles. Returns up to 10 results. Query parameters: `q` (string; required), `locale` (string) Headers: `X-Channel` (string) Responses: `200`, `429` ### Ecom.Cart The cart hangs off the session, so there is no cart id to pass around. Anonymous sessions have carts too, and the cart survives signing in. #### `GET /ecom/cart` — Get shopping cart Returns the current shopping cart contents with product details. Headers: `X-Channel` (string) Responses: `200`, `401`, `429` #### `DELETE /ecom/cart` — Clear shopping cart Removes all items from the shopping cart. Headers: `X-Channel` (string) Responses: `204`, `401`, `404`, `429` #### `POST /ecom/cart/items` — Add item to cart Adds an item to the shopping cart or increases quantity if already present. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `429` #### `PATCH /ecom/cart/items/{itemId}` — Update cart item quantity Updates the quantity of an item in the shopping cart. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `404`, `429` #### `DELETE /ecom/cart/items/{itemId}` — Remove item from cart Removes an item from the shopping cart. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` ### Ecom.Checkout Checkout from available methods to placed order, including discount coupons, gift cards, shipping price calculation and postal code lookup. Payment runs through a provider-agnostic session: the response says how to render it, not which provider produced it. #### `POST /ecom/checkout/apply-code` — Apply code Validates a gift card or discount code for checkout use. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `404`, `429` #### `GET /ecom/checkout/data` — Get checkout data Returns available shipping methods, payment methods and pre-filled customer data for the checkout page. Headers: `X-Channel` (string) Responses: `200`, `401`, `429` #### `POST /ecom/checkout/place` — Place order Completes checkout and creates a sales order from the shopping cart. Send an Idempotency-Key header and a retry after a timeout replays the original response instead of placing a second order, or answering that the cart is already converted. The header is optional; without it the call behaves as it always has. Headers: `X-Channel` (string), `Idempotency-Key` (string) Request body: `application/json` (required) Responses: `201`, `400`, `401`, `422`, `429`, `503` #### `POST /ecom/checkout/sessions` — Create checkout session Creates a checkout session at the channel's configured payment provider. Send an Idempotency-Key header and a retry after a timeout returns the original session instead of opening a second one at the provider. The header is optional. Headers: `X-Channel` (string), `Idempotency-Key` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `422`, `429`, `503` #### `PATCH /ecom/checkout/sessions/{sessionId}` — Update checkout session Refreshes an existing checkout session at its payment provider when the cart has changed. Headers: `X-Channel` (string) Responses: `200`, `400`, `401`, `404`, `429` #### `GET /ecom/checkout/sessions/{sessionId}/confirmation` — Get checkout confirmation Returns the payment provider's confirmation view and the resulting order number. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` #### `POST /ecom/checkout/shipping-prices` — Calculate shipping prices Calculates shipping prices for all enabled shipping methods based on destination country and cart weight using FreightPriceList. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `401`, `429` #### `POST /ecom/checkout/webhooks/{provider}` — Payment provider webhook Callback from a payment provider when a checkout completes. Creates the sales order. Idempotent. Headers: `X-Channel` (string) Responses: `200`, `429` #### `GET /ecom/postal-codes/lookup` — Verify a postal code and get its city Status is NoRegister, Verified, CityMismatch, UnknownCode or InvalidFormat. Advisory only — never blocks an order from being placed. Query parameters: `country` (string; required), `code` (string; required), `city` (string) Headers: `X-Channel` (string) Responses: `200`, `429` #### `GET /ecom/postal-codes/suggest` — Prefix search on postal code or city Terms shorter than two characters return an empty list. Query parameters: `country` (string; required), `q` (string; required), `take` (integer) Headers: `X-Channel` (string) Responses: `200`, `429` ### Ecom.Kco Klarna Checkout, the provider-specific predecessor of the checkout session endpoints. Still supported, but new builds should use Ecom.Checkout instead. The push endpoint here is an inbound callback from Klarna, not something a storefront calls. #### `POST /ecom/kco/push` — KCO push callback Webhook called by Kustom when checkout is completed. Creates a SalesOrder and acknowledges to Kustom. Idempotent. Headers: `X-Channel` (string) Responses: `200`, `429` #### `POST /ecom/kco/sessions` — Create KCO session Creates a Kustom Checkout session and returns an HTML snippet for embedding the checkout widget. Deprecated alias for POST /ecom/checkout/sessions. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `429` #### `PUT /ecom/kco/sessions/{sessionId}` — Update KCO session Updates an existing Kustom Checkout session when the cart has changed. Deprecated alias for PATCH /ecom/checkout/sessions/{sessionId}. Headers: `X-Channel` (string) Responses: `200`, `400`, `401`, `404`, `429` #### `GET /ecom/kco/sessions/{sessionId}/confirmation` — Get KCO confirmation Returns the Kustom Checkout confirmation HTML snippet and order details. Deprecated alias for GET /ecom/checkout/sessions/{sessionId}/confirmation. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` ### Ecom.Account Signing in: password, one-time code by email, and password reset. All of them upgrade the existing session rather than replacing it, so the cart carries over. #### `POST /ecom/auth/login` — Login Authenticates a customer contact using email and password. Associates the session with the customer. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `429` #### `POST /ecom/auth/one-time-login` — One-time code login Authenticates a customer contact using a one-time email code. Associates the session with the customer. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `429` #### `POST /ecom/auth/request-code` — Request auth code Sends a 6-digit verification code to the specified email address. Used for password reset and one-time login. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `429` #### `POST /ecom/auth/reset-password` — Reset password Resets the customer password using a verified 6-digit code. Auto-logs in the customer on success. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `429` #### `POST /ecom/auth/verify-code` — Verify auth code Validates a 6-digit code without consuming it. Returns validity status and remaining attempts. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `429` #### `GET /ecom/portal/context` — Get portal context Returns portal modules and permissions for the authenticated customer contact. Headers: `X-Channel` (string) Responses: `200`, `401`, `429` ### Ecom.Profile The signed-in customer's own contact details. #### `GET /ecom/portal/profile` — Get profile Returns profile details for the authenticated customer contact. Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` #### `PATCH /ecom/portal/profile` — Update profile Updates the authenticated contact's profile details. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `204`, `400`, `401`, `429` ### Ecom.Addresses The signed-in customer's delivery addresses, including which one is the default. #### `GET /ecom/portal/addresses` — List addresses Returns delivery addresses for the authenticated customer. Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` #### `POST /ecom/portal/addresses` — Create address Creates a new delivery address for the authenticated customer. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `401`, `403`, `429` #### `PATCH /ecom/portal/addresses/{id}` — Update address Updates an existing delivery address for the authenticated customer. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `204`, `400`, `401`, `404`, `429` #### `DELETE /ecom/portal/addresses/{id}` — Delete address Deletes a delivery address for the authenticated customer. Headers: `X-Channel` (string) Responses: `204`, `401`, `404`, `429` #### `POST /ecom/portal/addresses/{id}/set-default` — Set default address Sets a delivery address as the default for the authenticated customer. Headers: `X-Channel` (string) Responses: `204`, `401`, `404`, `429` ### Ecom.Orders The signed-in customer's order history and order details, with line level fulfilment status. #### `GET /ecom/portal/orders` — List orders Returns a paginated list of orders for the authenticated customer. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `20`), `status` (string) Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` #### `GET /ecom/portal/orders/{id}` — Get order detail Returns the full detail of a specific order for the authenticated customer. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` ### Ecom.Invoices The signed-in customer's invoices. #### `GET /ecom/portal/invoices` — List invoices Returns invoices for the authenticated customer. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `20`), `status` (string) Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` ### Ecom.Shipments Deliveries against the signed-in customer's orders, with carrier tracking where the carrier provides it. #### `GET /ecom/portal/shipments` — List shipments Returns a paginated list of shipments for the authenticated customer. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `20`), `status` (string) Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` #### `GET /ecom/portal/shipments/{id}` — Get shipment detail Returns the full detail of a specific shipment for the authenticated customer. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` ### Ecom.Quotes Quote requests: a signed-in customer asks for a price on a set of items, then accepts or declines what comes back. An accepted quote becomes a sales order in the ERP. #### `GET /ecom/portal/quotes` — List quotes Returns a paginated list of quotes for the authenticated customer. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `20`), `status` (string) Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` #### `POST /ecom/portal/quotes/request` — Request a quote Creates a new quote request from the customer portal. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `201`, `400`, `401`, `403`, `429` #### `GET /ecom/portal/quotes/{id}` — Get quote detail Returns the full detail of a specific quote for the authenticated customer. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` #### `POST /ecom/portal/quotes/{id}/accept` — Accept a quote Accepts a sent quote on behalf of the authenticated customer. Headers: `X-Channel` (string) Responses: `204`, `400`, `401`, `404`, `429` #### `POST /ecom/portal/quotes/{id}/decline` — Decline a quote Declines a sent quote on behalf of the authenticated customer. Headers: `X-Channel` (string) Request body: `application/json` Responses: `204`, `400`, `401`, `404`, `429` ### Ecom.Returns Customer-initiated returns: check what an order is eligible to return, register the return, print the label and follow it. Registering moves no stock — receiving at the warehouse does. #### `GET /ecom/portal/orders/{orderId}/return-eligibility` — Check return eligibility Checks whether an order is eligible for return and returns returnable line details. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` #### `GET /ecom/portal/returns` — List returns Returns a paginated list of returns for the authenticated customer. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `20`), `status` (string) Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` #### `POST /ecom/portal/returns` — Submit return Creates a new return request from the customer portal. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `201`, `400`, `401`, `403`, `429` #### `GET /ecom/portal/returns/{id}` — Get return detail Returns the full detail of a specific return for the authenticated customer. Headers: `X-Channel` (string) Responses: `200`, `401`, `404`, `429` #### `POST /ecom/portal/returns/{id}/cancel` — Cancel return Cancels a return request that has not yet been received. Headers: `X-Channel` (string) Responses: `204`, `400`, `401`, `404`, `429` #### `GET /ecom/portal/returns/{id}/label` — Get return label Downloads or redirects to the return shipping label for a return. Headers: `X-Channel` (string) Responses: `302`, `401`, `404`, `429` ### Ecom.Tickets Support cases the customer opens from their own pages, with the message thread and attachments. Cases land in the same queue as those created inside the ERP and from the support mailbox. #### `GET /ecom/portal/tickets` — List tickets Returns the authenticated contact's tickets. Contacts with the ViewAllTickets permission see every ticket belonging to their company. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `20`), `status` (string), `openOnly` (boolean; default `false`), `search` (string) Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `429` #### `POST /ecom/portal/tickets` — Create ticket Creates a support ticket for the authenticated customer and contact. Order and order line are optional; when given they are verified against the session's customer and the item is taken from the order line. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `201`, `400`, `401`, `403`, `429` #### `GET /ecom/portal/tickets/{id}` — Get ticket Returns one ticket with its attachments and customer-visible messages. Internal notes are never included. Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `404`, `429` #### `POST /ecom/portal/tickets/{id}/attachments` — Upload ticket attachment Uploads one file to the ticket. Multipart form data with the file in the 'file' field. Headers: `X-Channel` (string) Request body: `multipart/form-data` (required) Responses: `201`, `400`, `401`, `403`, `404`, `429` #### `GET /ecom/portal/tickets/{id}/attachments/{attachmentId}` — Download ticket attachment Downloads one attachment from a ticket the contact is allowed to see. Headers: `X-Channel` (string) Responses: `200`, `401`, `403`, `404`, `429` #### `POST /ecom/portal/tickets/{id}/comments` — Reply on ticket Adds a customer-visible message to the ticket thread. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `201`, `400`, `401`, `403`, `404`, `429` #### `PATCH /ecom/portal/tickets/{id}/product` — Supply product details on ticket Lets the customer fill in serial number, firmware and hardware revision. The completion is added to the ticket thread so the agent sees it arrive. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `204`, `400`, `401`, `403`, `404`, `429` ### Ecom.Reviews Product reviews: read the approved ones, submit a new one. Submissions are anonymous-capable and go through moderation before they appear. #### `GET /ecom/catalog/products/{idOrSlug}/reviews` — Get product reviews Returns a page of approved reviews plus a rating summary (average and distribution) covering all approved reviews for the product on this channel. Query parameters: `page` (integer; default `1`), `pageSize` (integer; default `10`), `sort` (string) Headers: `X-Channel` (string) Responses: `200`, `404`, `429` #### `POST /ecom/catalog/products/{idOrSlug}/reviews` — Submit product review Submits a review for a product on the current channel. The review is always created as pending and is not visible until a moderator approves it. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `404`, `409`, `429` ### Ecom.Newsletter Newsletter sign-up with double opt-in, and one-click unsubscribe per RFC 8058. The unsubscribe endpoint is quota'd per token rather than per IP, because mail providers share addresses across many recipients. #### `POST /ecom/newsletter/confirm` — Confirm newsletter subscription Confirms a pending newsletter subscription using the token from the confirmation email. The token is consumed and cannot be reused. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `404`, `409`, `429` #### `POST /ecom/newsletter/subscribe` — Subscribe to newsletter Signs an email address up for the channel newsletter. Always double opt-in: a confirmation email is sent and the subscription is not mailable until the link is followed. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `404`, `429` #### `POST /ecom/newsletter/unsubscribe` — Unsubscribe from newsletter Withdraws newsletter consent using the token from an email, passed as a query parameter. Requires no login — one-click unsubscribe per RFC 8058. Idempotent; any request body is ignored. Query parameters: `token` (string) Headers: `X-Channel` (string) Responses: `200`, `400`, `404`, `429` ### Ecom.Chat The shopping assistant: a server-sent event stream that answers product questions against the channel's own catalogue and content. #### `POST /ecom/chat/stream` — Ask the shopping assistant Answers a shopper's question against the channel's own catalogue and content, and streams the reply as server-sent events rather than returning a body. Each event is a `data:` line carrying a JSON fragment; concatenate the text fragments as they arrive and stop at the terminating event. Because the response is a stream, the 200 has no schema to document — read the event format from the frames themselves. Requires a session, and is enabled per channel through the enableChatAssistant feature flag. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `200`, `400`, `429` ### Ecom.Analytics Page-view ingestion for the tenant's own storefront reporting, feeding the SEO and traffic views inside Fluit. It records what your storefront tells it; it is not a public analytics service. #### `POST /ecom/analytics/pageview` — Track page view Records a page view for analytics. Called server-side from ecom-frontend. Headers: `X-Channel` (string) Request body: `application/json` (required) Responses: `204`, `429`