Carts
A cart is a workspace with an owner and a status. It is not a draft order: the conversion to an order is one explicit call, and after it the cart is closed and readable forever as the record of what became what.
Ownership
curl -X POST https://api.revenexx.com/v1/carts \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"contact_id":"…","name":"August reorder","is_current":true}'
The one thing a cart requires is an owner — contact_id for a signed-in customer or session_key for a guest, never neither. Everything else is defaulted: the name Cart, currency EUR, status active, both totals 0.
No column is unique, so one owner may hold as many carts as they like — unless the tenant's multi_cart_enabled is off, in which case a second active cart for the same owner answers 409 naming the cart that already exists (a storefront that hits that wants to fill that cart).
is_current: true makes the new cart current in the same call, clearing the flag on every sibling of the same owner.
The lifecycle
┌──── activate ──── (is_current only, status unchanged)
│
active ───── abandon ────► abandoned ───── reopen ─────► active
│
├──── order ──────────► ordered (final)
└──── merge-into ─────► merged (final)
| Status | Meaning |
|---|---|
active | The workspace. |
abandoned | Timed out or abandoned by hand. The one reversible ending. |
ordered | Handed to order management. Final; no route deletes it. |
merged | Folded into another cart. Final. |
POST /v1/carts/{id}/activate writes exactly one thing: is_current on this cart, cleared on every other cart of the same owner. It does not change the status, and only an active cart may be made current. Read it back with GET /v1/carts?is_current=true plus the owner — that filter is the only way to see what this route wrote.
POST /v1/carts/{id}/abandon moves an active cart to abandoned, stamps abandoned_at and clears is_current. The lines are untouched throughout.
POST /v1/carts/{id}/reopen takes an abandoned cart back to active with its lines exactly as they were — what a storefront calls when a buyer follows a recovery mail. It also clears abandoned_at, so the abandonment funnel counts carts that are still abandoned. It does not restore is_current.
ordered and merged are final and answer 400 naming the status the cart holds.
Items
curl -X POST "https://api.revenexx.com/v1/carts/{cart_id}/items" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"sku":"ACME-4711-BLK","quantity":6,"unit_price":19.90,"currency":"EUR"}'
| Route | Does |
|---|---|
GET /v1/carts/{cart_id}/items | List the lines. |
POST /v1/carts/{cart_id}/items | Add one. |
PUT /v1/carts/{cart_id}/items | Replace all lines in one call. |
GET/PUT/DELETE /v1/carts/{cart_id}/items/{id} | One line. |
A line's type is product, configuration or custom. A line carries unit_price, line_total, tax_rate, unit, position, and a snapshot of what the buyer was shown. A plain product line with the same product/sku and the same unit_price folds into an existing line by adding its quantity; configured and custom lines always land as new rows.
Claiming a guest cart at login
curl -X POST https://api.revenexx.com/v1/carts/claim \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"session_key":"sess_…","contact_id":"…"}'
The route a storefront fires the moment somebody signs in with a basket already filled. It hands every active cart of one session_key to a contact_id, and the body picks between two landings:
- Without
target_cart_idthe session carts are adopted as they stand — same carts, same lines,contact_idset andsession_keycleared. Nothing copied, nothing closed. - With
target_cart_idthey are folded into that cart, which survives while each session cart is closed asmerged.
The response's adopted and merged arrays say which happened to each. With a target, the tenant's cart_merge_strategy decides what happens to the target's own lines: merge keeps them and folds the session lines in, replace clears them first. Send strategy to override it for one call; the answer echoes which ran and how many lines a replace removed.
Merging
Which cart survives is the whole question, and the answer is the target.
| Route | Source | Use when |
|---|---|---|
POST /v1/carts/merge | source_cart_id in the body | You hold both ids. |
POST /v1/carts/{id}/merge-into | the path | You hold one id — a Cockpit row action, a detail page. |
The source's lines are copied into the target; the target keeps every line it already had and its totals are recomputed. The source keeps its own line rows and is closed with status: "merged" and merged_into_cart_id pointing at the target, so a merged cart stays readable as the record of what went where.
merge-into, read the path id as "the cart I am giving away". Getting the two the wrong way round is the mistake this route exists to make hard.Both carts must be active and must differ. The tenant's line limits are enforced on the target as the copies land (422).
Conversion to an order
curl -X POST "https://api.revenexx.com/v1/carts/{id}/order" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"order_ref":"ORD-000123"}'
An active cart becomes ordered, ordered_at is stamped, and order_ref — order management's own number — is stored on the cart, which is what lets anyone get from an order number back to the cart behind it. Nothing moves out of ordered.
The conversion applies the two tenant decisions a cart cannot make for itself:
price_snapshot_mode(snapshot|live) settles which of a line's two prices is charged — the snapshot the buyer was shown, or the currentunit_price— and the cart's subtotal is rewritten to match, so cart and order can never disagree. The response'spricingblock reports the mode, the lines it rewrote, and the subtotal on both sides.convert_reserves_stock(never|request|require) decides whether inventories is asked to hold the lines. Atrequirea refusal answers409and the cart stays active and unchanged. The reservation is attempted before anything is written.
This route does not create the order. POST /v1/orders/place does.
Bulk lines: import and export
The carts app keeps its own small import/export surface, separate from the shared /v1/io one.
curl -X POST https://api.revenexx.com/v1/carts/import \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"target_cart_id":"…","csv":"sku,quantity\nACME-4711-BLK,6\nACME-4712,2\n"}'
This is the bulk-order path a buyer pastes a spreadsheet into. With target_cart_id the lines land in that cart, which must be active, and the profile's apply_mode decides the fate of the lines already there (replace clears first; insert and append both add). Without a target a new cart is created, and an owner is then required.
profile_id names an import profile; without one the payload is read ad hoc — as CSV when csv is present, as JSON otherwise. Lines fold into identical product lines exactly as a single add does, so imported_lines counts the lines read and the cart may have gained fewer rows. A payload that parses to no line at all is a 400, not a quiet no-op.
curl -X POST "https://api.revenexx.com/v1/carts/{id}/export" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"format":"csv"}'
The JSON form is {cart: {…}, items: […]} and is exactly what carts/import takes back, so an export round-trips. The CSV form is the lines only, header first. Nothing is stored and nothing about the cart changes; a cart of any status can be exported, including one already ordered.
Profiles live at /v1/carts/io/profiles, with POST /v1/carts/io/profiles/defaults to seed the bundled templates. Handing an import profile to the export route is a 400 — a profile only runs the way it declares.
The maintenance sweep
POST /v1/carts/maintenance/run does two sweeps in one pass:
abandon_after_minutesmarks active carts untouched past the window as abandoned. Nothing else in the platform setsabandoned_at, so without this the abandonment funnel is empty by construction.cart_ttl_days/guest_cart_ttl_daysthen delete carts past their retention window, line items included. Both default to0(never).
An ordered cart is never touched at any setting — it is the source record of a sale. Send dry_run for the same counts and cart ids while writing nothing. The platform runs this on a schedule; it is idempotent, so calling it by hand between ticks is safe.
Where to go next
- Orders — what happens after the hand-over.
- Order lists — a saved collection that fills a cart.
- Stock — what
convert_reserves_stockcalls. - Storefront accounts — the login that triggers a claim.