Build reorder and quick order

Let a buyer reorder from a saved list or paste a CSV of SKUs and quantities — save order lists on your server, turn a list into a priced cart, report what was left out, and hand the cart to checkout.

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

PieceWhat it is
Order listA saved set of lines a buyer keeps. It is read, never emptied, so it converts again next week. See Order lists
CartWhat checkout works from. A list becomes one in a single call
OrderPlaced 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_id and 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 as X-Revenexx-Market on the market-dependent calls, the same as in Connect your own frontend.
The API key is not the buyer. A call made with a key alone is a back-office call: it is not narrowed to one buyer, and 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.

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

Request
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

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

Response
{
  "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, unless on_missing_article is fail (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 are include, skip and fail. By default (include) the line is taken along, and because it then has no price it ends up in skipped as no_current_price, the same as an unpriced product. skip (labelled "Leave it out and report it" in Cockpit) reports it as missing_article. fail refuses the whole call with 400 and names the unknown articles and any unpriced lines too. Ask the merchant which one they want, and show the reason you get.
  • mode is append or replace. Send it explicitly: when you leave it out, the market's cart_merge_mode setting decides, and a merchant who set replace turns a bare call into one that empties the cart. append adds 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. replace makes 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_id to get a new cart, and send it to work on an existing one. With replace and an existing cart_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. Keep cart_id in your server session and use it for checkout, or make it current with POST /v1/carts/<CART_ID>/activate.
  • Failures. An empty list is a 400, and so is a call where nothing at all could be priced. A 400 with tax_unresolved means no market could be worked out: send X-Revenexx-Market. A 400 can 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. 502 means prices could not be fetched: retry later, because the call never falls back to the saved price. Read the message to tell the 400s 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.

  1. 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 append merges them in the cart.
  2. 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.
  3. Convert it with POST /v1/orderlists/<LIST_ID>/cart and read skipped.
  4. 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/place does not read the cart, so read the lines with GET /v1/carts/<CART_ID>/items and send them again in items, together with the cart_id, the buyer, the addresses and the currency. The body is described in Orders. Then POST /v1/carts/<CART_ID>/order with {"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 skipped shown to the buyer, with a reason in plain words.
  • append for an existing cart, replace only for a new one.
  • A buyer's CSV validated on your server and sent through a list, not through carts/import.
  • The cart_id kept 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.
Was this page helpful?