Order lists

Saved, reusable position collections on revenexx — list kinds, positions as whole saved lines, and the route that turns a list into a cart.

An order list is a saved collection of positions a buyer keeps and orders again: a monthly reorder, a reagent list, a labelling list. It is not a cart and not a draft order — it is read, never emptied, so the same list converts again next month.

Base path: https://api.revenexx.com/v1/orderlists.

The list

Request
curl -X POST https://api.revenexx.com/v1/orderlists \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Monthly consumables",
        "owner_id": "…",
        "owner_name": "Anna Berger",
        "organization_id": "…",
        "kind": "shopping",
        "items": [
          { "name": "Cordless drill, black", "sku": "ACME-4711-BLK", "quantity": 6 },
          { "name": "Drill bit set", "sku": "ACME-9001", "quantity": 2 }
        ]
      }'

Three fields are required — name, owner_id, owner_name — and they are exactly the columns the database will not fill in. Everything else has an answer already: kind resolves to your value, else the market's default_kind setting, else the kind the tenant flagged; shared is false; organization_id is null, which makes shared meaningless because there is then nobody to share with.

Nothing about a list is unique. One owner may keep two lists with the same name, and the same article may appear in as many lists as the buyer wants.

The optional items array creates the list pre-filled, in one request rather than a create plus twenty adds — and the array order is the position order. Those initial positions are normalised and article-checked before the list row is written, and both caps are checked first as well (max_items_per_list against the array, max_lists_per_owner against what this contact already keeps), so a rejected position never leaves an empty list behind.

The owner is set at creation. Only POST /v1/orderlists/{id}/transfer-owner moves a list to another contact (see Hand a list to a colleague).

Positions

Request
curl -X POST "https://api.revenexx.com/v1/orderlists/{list_id}/items" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Drill bit set","sku":"ACME-9001","quantity":2,"cost_center_id":"CC-4711"}'

A position is a whole saved line, not a pointer at a product. name is required and one of product_id / sku must be set; everything else is a snapshot of what the buyer saw: quantity, unit, price, tax_rate, image, custom_sku, cost_center_id, position_texts, category_slug, subcategory_slug, metadata.

Nothing here deduplicates. Adding the same article twice makes two positions, because it is the cart that merges lines by product and price, not the list.

currency is an optional ISO 4217 code for the currency price is in. Lower case is accepted and upper-cased; anything that is not three letters is a 400. Omit it on a position with a price, and the prices app's quoted currency is used (the market's, else the tenant's default), or EUR when that app cannot be asked. A position without a price gets EUR. Like price, it is informational: the cart conversion takes its own currency.

A new row takes the list's current position count unless the payload names a position, so it collides with an existing number whenever an earlier position was deleted from the middle. Adding one touches the list's updated_at, which is what the default sort of GET /v1/orderlists reads.

RouteDoes
GET /v1/orderlists/{list_id}/itemsList the positions. Filter on currency (exact match, an upper-case ISO 4217 code as stored).
POST /v1/orderlists/{list_id}/itemsAdd one.
PUT /v1/orderlists/{list_id}/itemsReplace all positions.
GET /v1/orderlists/{list_id}/items/{id}Read one position. The list in the path must own it: a position of another list answers 404.
PUT/DELETE /v1/orderlists/{list_id}/items/{id}Change or remove one position.
GET /v1/orderlists/{id}The list with its positions.

PUT /v1/orderlists/{list_id}/items/{id} is a partial update: omitted fields keep their value, and null clears one, except currency. It takes currency, the ISO 4217 code price is in (upper case). A null or empty currency is dropped and the stored code stays, because the column always holds a code (EUR by default). The fallback to the prices app's currency, or EUR when it cannot be asked, applies only when a position is created with a price and no currency, not on this single-item PUT. The currency is informational, like price: the cart conversion takes its own.

Kinds

A kind is the tenant's own taxonomy for sorting saved lists. Nothing in the app branches on the value, which is why a merchant can add one without a release.

Request
curl -X POST https://api.revenexx.com/v1/orderlists/kinds \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{"code":"reagents","title":"Reagent lists","tone":"info","position":30}'

code and title are required. The code is lowercased on the way in and immutable afterwards — renaming it would orphan every list carrying it, since a list stores the code and not the id. tone is one of neutral, info, success, warning, danger, for the badge a list row shows.

is_default: true promotes the new kind and demotes whoever held the flag; POST /v1/orderlists/kinds/{id}/make-default does the same later. Creating a kind changes no existing list.

POST /v1/orderlists/defaults seeds the two kinds a fresh tenant starts with — shopping (default) and label. Idempotent by code: created names what this call wrote, existing what was already there and was left exactly as the tenant keeps it, renames included. The install event runs it, and the first read of GET /v1/orderlists/kinds on an empty table seeds before answering — so you rarely need it.

PUT /v1/orderlists/kinds/{id} edits a kind's words: title, description, labels, descriptions, tone, position and is_default. The code cannot be changed, and sending a different one is a 400. Renaming a kind changes no list. active: false retires the kind from new lists, and the lists that carry it keep it. The default kind cannot be deactivated.

GET /v1/orderlists/kinds/{id} reads one kind by its id. A list stores the kind's code, not this id, so join a list to its kind on code.

DELETE /v1/orderlists/kinds/{id} removes a kind that no list uses. It answers 409 with kind_in_use while any list carries its code, and 409 when it is the last kind, because a list must have one. Nothing cascades and no list is rewritten. Deleting the default kind hands the default flag to the first active kind in position order, falling back to any kind when none is active. The answer is {deleted, id}. The delete does not touch the market's default_kind setting, so that setting can name a kind that no longer exists. A create that falls back to the setting then uses the tenant's default kind instead (the one flagged is_default), so creates keep working. A retired kind falls back the same way. A client that needs a particular kind sends kind on the create, and an operator should point the market's default_kind at a kind that still exists.

Hand a list to a colleague

When the buyer who kept a list has left, POST /v1/orderlists/{id}/transfer-owner with owner_id hands it to another contact of the same organization. Positions, kind and sharing stay as they were. The new owner must be a contact the customers app knows and, for a list that belongs to an organization, one of that organization's contacts. Back office only: a call that acts for a contact is refused with 403. This is the only way to change a list's owner; PUT /v1/orderlists/{id} refuses owner_id.

Turning a list into a cart

Request
curl -X POST "https://api.revenexx.com/v1/orderlists/{id}/cart" \
  -H "X-Revenexx-Tenant: <TENANT_SLUG>" \
  -H "X-Revenexx-Api-Key: rvxk_..." \
  -H "Content-Type: application/json" \
  -d '{"mode":"append"}'

Every position is sent to the cart in one call; lines that cannot be priced are reported, not added.

  • Sending cart_id adds to that existing cart.
  • Omitting it creates a cart for the list's owner — not for whoever called — and names it after the list. It becomes that contact's current cart only when the call carries an acting contact; with an API key alone it is not marked current, and you activate it yourself. A cart the buyer cannot see is not "added to cart".

Which happened is not left to be inferred: cart_created says so and cart_id names the cart either way.

append (the default, tenant-configurable through cart_merge_mode) lets the carts app merge each line by product and price so quantities accumulate. replace makes the list the cart's whole contents in one call.

What the cart has no column for — cost centre, custom SKU, position texts — rides in each line's snapshot, together with the list it came from.

The list itself is never touched.

A list does not become an order directly

There is no route that places a list as an order (as of 2026-10). The old one answered at the prices saved on the list and skipped approvals and the cost-center budget, so it was removed, and a call to it answers 404. A list becomes a cart, and the cart goes through checkout, where an order's approvals and budgets apply. The steps are in Build reorder and quick order.

Vocabularies

GET /v1/orderlists/vocabularies lists the names this app publishes. Today the only name is kinds. GET /v1/orderlists/vocabularies/{name} returns its values.

RouteDoes
GET /v1/orderlists/vocabulariesLists the names, each with its title and description. No values.
GET /v1/orderlists/vocabularies/{name}One vocabulary with its values, in position order.

For kinds the values are the tenant's own rows, so a kind you add appears here without a release. values holds the active kinds only, so on values active is always true. A retired kind comes back in a separate inactive array, where active is false; a UI showing a list that carries a retired kind reads inactive for its words. Each value carries its key, title, description, badge tone, labels, descriptions, is_default, is_system (set on the kinds seeded at install) and active. title and description are plain strings. The translations are locale maps keyed by language tag in labels and descriptions: read the tag you want there and fall back to title.

Where to go next

Was this page helpful?