Build reorder and quick order
This guide is for the developer building the storefront side of a B2B shop whose buyers reorder the same few dozen SKUs every week. By the end your server can save and read a buyer's order lists, turn a list or an uploaded CSV into a priced cart, tell the buyer which lines could not be added and why, and know what to carry across when you hand the cart to checkout. The checkout screen itself is yours to build.
It assumes a buyer who is already signed in on your server (Sign a buyer in from your server) and a frontend that calls the API from that server (Connect your own frontend).
The short answer: a saved list is turned into a cart by one call, which prices every line again at that moment. Lines that cannot be priced are left out and reported, and a call can add only some of the lines, so always read the answer (step 2). A CSV goes through a list too, after your server has checked it, so it gets the same pricing and the same report.
How the pieces fit
| Piece | What it is |
|---|---|
| Order list | A saved set of lines a buyer keeps. It is read, never emptied, so it converts again next week. See Order lists |
| Cart | What checkout works from. A list becomes one in a single call |
| Order | Placed from the cart at checkout. A list never becomes an order directly: there is no call for it (as of 2026-10). Approval and budget rules for orders are not covered by this guide |
Before you start
- The Order Lists, Carts, Prices and Orders Apps installed in the tenant.
- A server-side API key that covers them (which scope covers which app). Keep it on your server.
- The signed-in buyer's
contact.id,organization_idand name from the session your server holds. Take them from there, never from a value the browser sent. - The market code you sell in (
de), sent asX-Revenexx-Marketon the market-dependent calls, the same as in Connect your own frontend.
GET /v1/orderlists returns every list in the tenant. Your server must apply the buyer's ids to every call, as described in step 1.1. Save and read a buyer's lists
Create a list for the signed-in buyer. owner_id is the buyer's contact.id. A list has three required fields: name, owner_id and owner_name. The name is the buyer's name from the session.
curl -X POST https://api.revenexx.com/v1/orderlists \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Weekly consumables",
"owner_id": "<CONTACT_ID>",
"owner_name": "Anna Berger",
"organization_id": "<ORGANIZATION_ID>",
"items": [
{ "name": "Cordless drill, black", "sku": "ACME-4711-BLK", "quantity": 6 },
{ "name": "Drill bit set", "sku": "ACME-9001", "quantity": 2 }
]
}'
Each line needs a name and either a sku or a product_id, and a quantity above zero. The order of items is the order of the list. Two limits are settings of the Order Lists App that the merchant sets per market in Cockpit: max_items_per_list (default 200, at most 1,000) and max_lists_per_owner (default 100). Going over either answers 400 with the limit in the message, and nothing is saved.
To show a buyer their lists, filter by owner on every read, and by organization_id for lists shared inside a company. The filter is something you send, not something the platform enforces for you:
curl "https://api.revenexx.com/v1/orderlists?owner_id=<CONTACT_ID>" \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY"
Your server decides what a buyer sees: their own lists, plus the lists of their organization that have shared: true. Check the owner_id and shared of every list the answer holds before you pass it on.
Do the same check on every route that takes an id: reading one list, changing it, converting it, deleting it, and activating a cart. Load the list first and compare it with the session. A buyer may read and convert their own lists and the shared lists of their organization. Only the owner may change or delete a list, unless the market setting shared_lists_editable is on. The Order Lists App enforces this itself (403) only when the call carries an acting contact. A call made with a key alone has none, so it would let you change anyone's list, and this check is yours. The same goes for a cart_id: the conversion does not check whose cart it is, so pass only a cart your server created for this buyer. The same applies to the other cart routes.
2. Turn a list into a cart
curl -X POST https://api.revenexx.com/v1/orderlists/<LIST_ID>/cart \
-H "X-Revenexx-Tenant: $REVENEXX_TENANT" \
-H "X-Revenexx-Api-Key: $REVENEXX_API_KEY" \
-H "X-Revenexx-Market: de" \
-H "Content-Type: application/json" \
-d '{ "currency": "EUR", "mode": "append" }'
The answer tells you what happened:
{
"list_id": "…",
"cart_id": "…",
"cart_created": true,
"mode": "append",
"added": 38,
"skipped": [
{ "id": "…", "sku": "ACME-0001", "name": "Old fitting", "reason": "no_current_price" }
]
}
What to know before you build the screen:
- Prices are asked again, now. The price saved on the list is a memory of what the buyer once saw, and it is never used. A line the buyer has no price for is left out with the reason
no_current_price, unlesson_missing_articleisfail(next point). That also happens to a product that exists but is not priced for this buyer, so do not tell the buyer the SKU is wrong. - A SKU the catalog does not know (a typo in a CSV, or a product that has left the catalog) is handled by a market setting,
on_missing_article, which the merchant sets in the Order Lists App's settings form in Cockpit. The values areinclude,skipandfail. By default (include) the line is taken along, and because it then has no price it ends up inskippedasno_current_price, the same as an unpriced product.skip(labelled "Leave it out and report it" in Cockpit) reports it asmissing_article.failrefuses the whole call with400and names the unknown articles and any unpriced lines too. Ask the merchant which one they want, and show the reason you get. modeisappendorreplace. Send it explicitly: when you leave it out, the market'scart_merge_modesetting decides, and a merchant who setreplaceturns a bare call into one that empties the cart.appendadds each line to the cart and merges it into an existing line for the same product and price, so quantities add up. It is not all-or-nothing: if line 20 fails, lines 1 to 19 stay in the cart.replacemakes the list the whole cart in one step and the carts App checks the lines and limits before it removes anything, but removing and re-adding is not guaranteed to be one transaction (unconfirmed), and it discards what was in the cart. Use it only on a cart you just created.- Omit
cart_idto get a new cart, and send it to work on an existing one. Withreplaceand an existingcart_id, the cart's lines are replaced by the list's. With a key alone, the cart and the prices are those of the list's owner, not of the signed-in buyer, and the cart is not marked as a current cart (as of 2026-10). So when a colleague converts an organization's shared list, they get the owner's cart and the owner's prices. Convert only lists the buyer owns, or accept owner pricing. Keepcart_idin your server session and use it for checkout, or make it current withPOST /v1/carts/<CART_ID>/activate. - Failures. An empty list is a
400, and so is a call where nothing at all could be priced. A400withtax_unresolvedmeans no market could be worked out: sendX-Revenexx-Market. A400can also mean the carts App rejected the cart, for example an inactive cart, a currency that does not match the cart's, or a cart limit.502means prices could not be fetched: retry later, because the call never falls back to the saved price. Read the message to tell the400s apart. - Do not convert twice. With
append, a second call adds the quantities a second time, so a double click doubles the cart. Disable the button, and keep a note in the session that this list was converted into this cart.
Show skipped to the buyer as a list they can act on: the line, and a plain sentence for the reason. A reorder that silently drops a line is worse than one that fails.
3. Quick order from a CSV
Do not send the buyer's CSV to the cart directly. Read it on your server, check each row, and then use the same conversion as for a saved list, so a pasted CSV gets the same prices and the same report.
- Parse the file on your server. Expect a header row and UTF-8, and accept a comma or a semicolon as the separator, because a spreadsheet in a German locale writes semicolons. Keep two columns: SKU and quantity. Check the whole file before you call the API, and reject it with the row numbers of the rows that are wrong: a quantity that is not a number, or zero or less, or an empty SKU. A list holds 200 lines by default, so split a larger file or tell the buyer the limit. The same SKU twice in one file is saved as two lines, and
appendmerges them in the cart. - Save a list for the upload with the same call as in step 1. A line needs a
name, and a CSV rarely has one: use the SKU as the name, since it is only a label. - Convert it with
POST /v1/orderlists/<LIST_ID>/cartand readskipped. - Delete the list with
DELETE /v1/orderlists/<LIST_ID>once you have the cart, because every list counts against the limit of lists per owner. Keep it when the buyer chooses "save this as a list".
POST /v1/carts/import also reads a CSV into a cart, on the server. It is meant for lines you trust, and it does not check SKUs against the catalog and does not price them. A line without a unit_price gets a price of 0. A bad row stops the import, and the lines read before it stay in the cart. Rows with neither a name nor a sku are dropped without a message. Do not use it for a file a buyer uploaded.4. Hand the cart to checkout
The cart is what the buyer reviews. Two things from this guide matter for the step after it:
- A cart line has no cost center column. The cost center, the customer's own article number and any position text from the list travel in the line's
snapshot. Your checkout has to read them from there if the order needs them. - Placing the order is a separate call.
POST /v1/orders/placedoes not read the cart, so read the lines withGET /v1/carts/<CART_ID>/itemsand send them again initems, together with thecart_id, the buyer, the addresses and the currency. The body is described in Orders. ThenPOST /v1/carts/<CART_ID>/orderwith{"order_ref": "<ORDER_ID>"}records the order on the cart and freezes it. The carts App never places the order itself. If the second call fails, the order exists and the cart is still active, so retry only the second call, never the placement.
An order can come back with the status pending and no placed_at instead of placed. With a key alone, /v1/orders/place applies only the tenant's order-value threshold, the purchase-request flow is a separate route, and how a pending order is released is unconfirmed, so do not promise the buyer an approval step from this call alone. See Set up purchase approvals and Give buyer staff the right permissions, and Carts for the lifecycle.
Checklist
- Every list read and write filtered by the signed-in buyer's ids, on your server.
- The conversion answer's
skippedshown to the buyer, with a reason in plain words. -
appendfor an existing cart,replaceonly for a new one. - A buyer's CSV validated on your server and sent through a list, not through
carts/import. - The
cart_idkept in your session, and the cart activated if you need it to be the current one.
Where to go next
- Order lists — every field of a list and a position.
- Carts — lifecycle, merging and conversion to an order.
- Set up B2B pricing — why the same SKU prices differently per buyer.