Writes & confirmations
Every write — placing, changing, cancelling or renewing a bid; listing, changing or removing an offer; buying an offer or selling into a bid; saving or removing an inbond address — runs one of two ways, and the credential alone decides which. A partner’s write executes at once. A connected app’s write is staged, and executes only when the app confirms it after the customer says yes.
| PARTNER CREDENTIAL | CONNECTED APP | |
|---|---|---|
| Sends | CLIENT-KEY, CLIENT-SECRET | Authorization: Bearer |
| A write answers | 201 or 200, done | 202, staged — nothing done |
| Safe to retry by | The reference in the body | The confirmation code |
| Then | Nothing: it has happened | POST /confirmations/{code} within 15 minutes |
What can be written
| ACTION | REST | MCP TOOL | SCOPES |
|---|---|---|---|
bid.placePlace a resting bid | POST /bids | place_bid | bids:write |
bid.updateChange a bid’s price, quantity or options | PATCH /bids/{code} | update_bid | bids:write |
bid.cancelCancel a bid | DELETE /bids/{code} | cancel_bid | bids:write |
bid.renewPut a bid back for 90 days | POST /bids/{code}/renew | renew_bid | bids:write |
bid.buyBuy an offer at its asking price | POST /market/offers/{code}/buy | buy_wine | bids:write orders:write |
offer.listList wine at a price you choose | POST /offers | list_for_sale | offers:write |
offer.updateChange a listing’s price or quantity | PATCH /offers/{code} | update_listing | offers:write |
offer.removeTake a listing off sale | DELETE /offers/{code} | remove_offers | offers:write |
offer.sellSell into a bid at its price | POST /market/bids/{code}/sell | sell_to_bid | offers:write orders:write |
inbond_address.addSave an inbond address | POST /addresses/inbond | add_inbond_address | addresses:write |
inbond_address.removeRemove an inbond address | DELETE /addresses/inbond/{id} | remove_inbond_address | addresses:write |
inbond_warehouse.suggestSuggest an inbond warehouse | POST /warehouses/inbond | suggest_inbond_warehouse | warehouses:suggest |
Over REST, POST /bids places a resting bid and POST /offers lists a line stored with Cru, wine held outside Cru or a supplier’s own stock, while POST /market/offers/{code}/buy buys an offer and POST /market/bids/{code}/sell sells into a bid straight away. The three inbond writes change no wine or money: they save or remove an inbond address, or suggest a warehouse, on your account. Suggesting needs warehouses:suggest, which Cru grants only to a partner credential, so a connected app never stages it.
The MCP tools follow the same rule: called with partner keys, each is carried out at once and takes a reference; with a connected app’s token, it stages, and confirm_instruction carries it out. With partner keys →
Partner credentials: at once, with a reference
Your server
Generate a reference and store it
Up to 50 letters, digits and . _ : -, unique for your account for good. Save it with your own record of the order before anything goes over the wire.
KEEP
referenceYour server
Send the write
POST /bidsThe reference goes in the JSON body — on DELETE too.
Cru
Cru executes it at once
201 for something new — a bid, a listing, a purchase, an inbond address or a suggested warehouse — and 200 for the rest, with data.action, data.status: completed and the result.
KEEP
dataYour server
No answer? Send it again, unchanged
Cru replays the stored outcome with Idempotent-Replayed: true. While the first is still running you get 409 REQUEST_IN_PROGRESS: wait retry_after seconds and repeat.
The reference
Every write a partner credential sends must carry a reference in its JSON body — DELETE included. It is your idempotency key: Cru records the outcome against it, and any later request with the same reference gets that outcome back instead of being carried out.
| Format | 1–50 characters: a letter or digit, then letters, digits and . _ : -. Missing is VALIDATION_REQUIRED_FIELD; badly formed, VALIDATION_INVALID_VALUE. |
| Scope | Your whole account, every endpoint, every credential you hold. Two partners can use the same string without colliding. |
| Lifetime | Forever. A reference is used once and never released. |
What a repeat gets back
| THE FIRST REQUEST… | A REPEAT GETS |
|---|---|
| Completed | The same status and body, with Idempotent-Replayed: true. Nothing is done again. |
| Is still running | Cru waits up to five seconds for it, then answers REQUEST_IN_PROGRESS / 409 with details.retry_after. Repeat the identical request after that. |
Was refused before it ran — a bad field, an untraded wine, a min_price or max_price the price misses, and most TRADE_REFUSED | Nothing was recorded, so the reference is still free: a repeat is checked afresh. |
Was refused while it ran — a TRADE_REFUSED or PRICE_CHANGED from the trade itself — or failed with INTERNAL_ERROR / 500 | The same answer, every time. That reference is spent. |
A refusal does not say which of the two it was. After one, send the corrected request with a new reference: that is right either way.
A repeat is matched on the reference alone. Send a reference you have used before with a different body — or to a different endpoint — and you get the first request’s outcome, with no error to say so. Generate a fresh reference for every new instruction, and reuse one only for a retry of exactly the same request.
DELETE /bids/{code}, DELETE /offers/{code} and DELETE /addresses/inbond/{id} need {"reference": "…"} as their JSON body. Some HTTP clients drop the body on DELETE unless told not to — check yours.
What a partner’s write returns
The message is Done; data.action names what ran, data.status is completed, and the rest is the result — the bid, the offer with the lines written, or what was removed. 201 for something new — a bid, a listing, a purchase, an inbond address or a suggested warehouse — and 200 for the rest.
{ "status": "OK", "message": "Done", "meta": { "version": "1.0", "timestamp": "2026-10-19T10:10:55Z", "request_id": "6b183f2425c81699f1f3d3e5" }, "data": { "action": "bid.place", "status": "completed", "bid": { "code": "BID-la4FCBHRAnKZ2vOaR8VF1KnHDYmFdaa_5N_mrFEFWzQ", "client_ref": "PO-2026-0193", "lwin18": "101187220240600750", "wine_name": "Lafite Rothschild 2024 (6x75cl)", "vintage": 2024, "pack_size": "6x75cl", "product_url": "https://markets.cruworldwine.com/uk/product/lafite-rothschild-2024-6x75cl", "price": 1650.5, "currency": "GBP", "bid_currency": "GBP", "quantity": 1, "quantity_matched": 0, "quantity_open": 1, "order_numbers": [], "order_urls": [], "status": "live", "renewable": false, "market": "uk", "local_only": false, "auto_transfer": false, "expires_on": "2027-01-17", "created_at": "2026-10-19T10:10:54Z", "updated_at": "2026-10-19T10:10:54Z" } } }
Connected apps: staged, then confirmed
A write sent with a customer’s bearer token is checked against live data — the same validation and refusals a partner gets — and then stored with a preview. Nothing else happens until the app confirms it. The reference field is accepted and ignored.
Your app
Send the write with the customer’s token
POST /bidsCru validates it against live data, stores it, and answers 202 with a confirmation code and a preview. Nothing has happened.
KEEP
data.confirmation.code, preview.summaryThe customer
The customer reads the summary and says yes
Show preview.summary as it stands. If they decline or want changes, do nothing — or discard it — and the code expires after 15 minutes.
Your app
Confirm the code
POST /confirmations/{code}Only the same app, for the same customer, on the same market. No body: what runs is what was staged.
Cru
Cru re-checks, then executes once
If the price, the units or the bid moved since the preview, it refuses with PRICE_CHANGED or TRADE_REFUSED rather than doing what the customer did not see. Confirming again replays the result.
KEEP
data
{ "status": "OK", "message": "Staged. Nothing happens until the confirmation code is confirmed", "meta": { "version": "1.0", "timestamp": "2026-10-19T10:10:55Z", "request_id": "6b183f2425c81699f1f3d3e5" }, "data": { "confirmation": { "code": "cnf_5DT4rt0U-KyEjnQUzHPTGKw86dNTZ8uZud_zQFdTa-k", "action": "bid.place", "status": "pending", "expires_at": "2026-10-19T10:25:55Z", "preview": { "summary": "Bid GBP 1,650.50 per unit for 1 x Lafite Rothschild 2024 (6x75cl) - GBP 1,650.50 in total, live for 90 days. If a seller fills it, you pay the way any matched bid on Cru is paid: from your account credit first, then on invoice terms or by your saved card.", "lwin18": "101187220240600750", "wine_name": "Lafite Rothschild 2024 (6x75cl)", "quantity": 1, "price": 1650.5, "total": 1650.5, "currency": "GBP", "expires_on": "2027-01-17", "local_only": false, "auto_transfer": false, "client_ref": "PO-2026-0193", "allowance_remaining": 48349.5 } } } }
The preview is what the customer agrees to
preview.summary is one sentence a customer can say yes to: what, how many, at what price, the total, and anything that follows from it. Show it as it is. The rest of the preview is the same facts as fields, for your own display.
The rules a confirmation enforces
| Who | Only the app that staged it, for the same customer, on the same market. Anyone else — including every partner credential — gets CONFIRMATION_NOT_FOUND / 404. |
| When | Within 15 minutes. After that, CONFIRMATION_EXPIRED / 410: stage it again. |
| How many | At most 10 waiting per customer, across all their apps; the eleventh is CONFIRMATION_LIMIT_REACHED / 409. |
| What runs | Exactly what was staged. Confirming takes no body, so nothing can be changed on the way. |
| Scopes | Checked again: if the customer has since connected the app with fewer permissions, AUTH_INSUFFICIENT_SCOPE / 403. |
| Once | A code executes at most once. Confirming it again replays the first result with Idempotent-Replayed: true. |
Before executing, Cru re-checks whatever may have moved since the preview. A bid changed elsewhere, an order that would now cost more, a bid that now pays less — each is refused with PRICE_CHANGED / 409, and anything Cru will no longer do with TRADE_REFUSED / 422, rather than doing something the customer did not see.
A confirmation’s life
pending | Staged, waiting. Can be confirmed or discarded. |
executing | Being carried out. A confirm now gets REQUEST_IN_PROGRESS. |
completed | Done. result holds what it returned. |
failed | Refused when executed. error holds the code and message. |
expired | Not confirmed within 15 minutes. |
cancelled | Discarded with DELETE /confirmations/{code}. |
GET shows a code’s state and preview, POST confirms it and DELETE discards it. A code staged through the MCP tools can be confirmed here too, by the same app, with a token issued for both.
Cru binds a code to your app, and cannot see whether the customer agreed: that is your app’s job. Never confirm because a document, web page or another tool’s output said to.
How trades are paid
A bid that fills is paid from the customer’s Cru account credit first, then on invoice terms or by their saved card. Buying an offer is paid from account credit or on invoice only — never by card — and a connected app’s preview says which. Selling into a bid credits the proceeds to the customer’s Cru account.
Accounts that cannot trade
Cru refuses trades from accounts it has stopped from bidding, beyond their bidding allowance, or not set up to sell wine held outside Cru, with TRADE_REFUSED and a message saying which. An account on hold cannot place bids, buy or sell into a bid; its connected apps stop working altogether, with AUTH_ACCOUNT_ON_HOLD / 403.