Orders
Order endpoints need the ORDERS scope.
Place an order
Section titled “Place an order”POST /orders with an Idempotency-Key header. The response is 201 with the order.
{ "externalOrderId": "ORD-1001", "externalRecipientId": "player-77", "externalCampaignRef": "vip-october", "externalActionRef": "deposit-milestone-5", "brandId": "0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b", "items": [ { "productId": "9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d", "variations": { "Colour": "Black" } } ], "recipient": { "name": "Wendy Player", "phone": "+4930123456", "address": { "street": "Unter den Linden 1", "city": "Berlin", "postalCode": "10117", "country": "DE" } }, "courierComment": "Ring twice", "urgent": false}Fields
Section titled “Fields”| Field | Rules |
|---|---|
externalOrderId |
Required. Your order ID, max 128 characters. Unique within the tenant, across all brands. |
externalRecipientId |
Your player ID. Optional, max 128 characters. Usable as a filter. |
externalCampaignRef, externalActionRef |
Your references for reporting. Optional, max 128 characters each. |
campaignId |
Order from a campaign. The order uses the campaign’s brand. |
brandId |
Required without campaignId. With campaignId, omit it or send the campaign’s brand. |
items |
Exactly one item: {productId, variations} for a catalog gift, or {customProductName, budget} for a custom request. |
client |
Required. name is required, email is optional. The person the gift is for. |
recipient |
Required. Delivery details: name (max 100), phone and address with street, city and country. address.postalCode is required for most countries. email is optional. |
managerEmail |
A team member who owns the order. Unknown answers 422 manager_not_found. |
courierComment |
Note for the courier, max 500 characters. |
additionalDetails |
Note for Luxorr, max 2,000 characters. |
urgent |
Mark the order as urgent. |
afterHoursServiceAccepted |
The client accepts delivery work outside office hours. |
brandedPackagingRequested |
Use the brand’s packaging, where available. |
customization |
{details}, max 2,000 characters. Only for gifts with customizationAvailable. |
customFields |
Answers to the tenant’s order fields: [{fieldId, textValue, checked}]. GET /me lists the fields in tenant.customFields. An unknown fieldId is ignored. |
Client and recipient can differ: a VIP host orders for a player (client), and the gift ships to the player’s assistant (recipient).
Custom item: {"customProductName": "Signed match jersey", "budget": {"amount": "250.00", "currency": "EUR"}}. Cannot be combined with campaignId. Luxorr sources it and replies in the order messages.
Budget currency must be the tenant’s currency, tenant.currency in GET /me. Another currency answers 400 invalid_request on items[0].budget.currency, with the expected currency in the message.
Variations name one value per dimension, spelled as in the catalog.
Addresses and phone numbers
Section titled “Addresses and phone numbers”recipient.address.postalCodeis required for SE, NO, DK, FI, IS, DE, FR, NL, BE, AT, CH, ES, IT, PT, PL, GR, CZ, SK, HU, RO, BG, HR, SI, EE, LT, LV, LU, UA, US, CA, MX, BR, GB, IN, JP, SG, CN, KR, AU, NZ, TH, MY, ID, PH, VN, ZA, TR and IL. It is optional elsewhere, including Ireland. The format is not checked. Max 20 characters.- A missing postal code answers
400 invalid_requestwith{"field": "recipient.address.postalCode", "code": "required", "message": "recipient.address.postalCode is required for DE"}. recipient.phoneuses international format:+, country code, number, such as+4930123456. Letters, a missing+or country code, or a wrong length answerinvalidonrecipient.phone.streetmax 255 characters,citymax 100,countryISO 3166-1 alpha-2.
After the order is placed
Section titled “After the order is placed”- Approval. When the tenant requires approval, the order starts
REQUESTEDuntil a team member approves it in the workspace. Keys cannot approve. Otherwise it startsPROCESSING. - Owner. A team member named in
managerEmailowns the order. - Workspace. The order appears in the workspace, labelled “via API” with the key name.
See order statuses.
Errors
Section titled “Errors”| Code | Status | Meaning |
|---|---|---|
invalid_request |
400 | A field is missing or malformed. errors[] lists each field. |
idempotency_key_required |
400 | No Idempotency-Key header. |
idempotency_key_invalid |
400 | Idempotency-Key is longer than 255 characters. |
unsupported_media_type |
415 | The body is not application/json. |
not_acceptable |
406 | Accept excludes application/json. |
brand_required |
422 | Neither campaignId nor brandId. |
brand_not_found |
422 | The brand does not exist or is outside the key’s brands. |
brand_campaign_mismatch |
422 | brandId is not the campaign’s brand. |
campaign_not_found |
422 | The campaign does not exist or is outside the key’s brands. |
campaign_not_active |
422 | The campaign is not active. |
product_not_in_campaign |
422 | The campaign does not offer the gift. |
product_not_available |
422 | The gift cannot be ordered. |
not_deliverable_to_country |
422 | The gift cannot be delivered to recipient.address.country. |
invalid_variations |
422 | Not one valid value per dimension. |
invalid_custom_fields |
422 | A required custom field is missing, or an answer does not fit its field. |
manager_not_found |
422 | managerEmail is not an active team member. |
out_of_stock |
409 | No campaign stock left for these variations. |
external_order_id_conflict |
409 | The tenant already has an order with this externalOrderId. |
idempotency_key_reused |
422 | The Idempotency-Key was used with a different request. |
No error places an order, and none is a 5xx. Fix the request and send it with a new Idempotency-Key.
Field errors. Each errors[] entry has a field and a code:
required: missing or blank, such asrecipient.phone, orrecipient.address.postalCodewhere required;too_long: longer than allowed, such asexternalOrderIdover 128 characters;invalid: malformed or not allowed, such as an impossiblerecipient.phone, a wrong JSON type ("urgent": "yes"givesurgent must be true or false),brandIdonbrand_campaign_mismatch, ormanagerEmailonmanager_not_found.
Your order IDs
Section titled “Your order IDs”externalOrderId is unique within the tenant. Sending it again with a new idempotency key answers 409 external_order_id_conflict. orderId names the existing order when the key can see it:
{ "type": "https://docs.luxorr.io/errors#external_order_id_conflict", "title": "External order id already used", "status": 409, "code": "external_order_id_conflict", "detail": "An order with this externalOrderId already exists: see orderId", "orderId": "4c1f9a52-8d0e-4f4b-9a3e-1f2d3c4b5a69"}- Several systems, one brand. When a shop and a CRM each place orders with their own key, prefix the IDs per system (
SHOP-1001,CRM-88412) so they never collide. - Other brand. A key limited to “Example Brand” that sends the
brandIdof “Second Brand” gets422 brand_not_found. Reading an order of “Second Brand” answers404 not_found.
Find and read orders
Section titled “Find and read orders”GET /orders/{orderId}reads one order by Luxorr ID.GET /orders?externalOrderId=ORD-1001finds it by your ID.
GET /orders lists all orders within the key’s brands, from every channel, newest placed first. Filters: externalOrderId, externalRecipientId, status, brandId, updatedSince. Paging: after, limit.
updatedSince returns orders changed at or after that instant. The list stays sorted by placement time, newest first. The last item on a page is not the latest change.
- Call
GET /orders?updatedSince=<instant>&limit=100and follownextCursoruntil it isnull. - Replace your copy of each order. Keep the largest
updatedAtacross all pages. - Next time, pass that value minus a few minutes. Duplicates are harmless.
Send Z, or encode + as %2B. For real-time changes, use webhooks and events.
An order
Section titled “An order”{ "id": "4c1f9a52-8d0e-4f4b-9a3e-1f2d3c4b5a69", "reference": "G-2026004711", "version": 3, "externalOrderId": "ORD-1001", "externalRecipientId": "player-77", "externalCampaignRef": "vip-october", "externalActionRef": "deposit-milestone-5", "source": "API", "brandId": "0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b", "campaignId": null, "selection": null, "personalLinkId": null, "status": "SENT", "items": [ { "kind": "CATALOG", "productId": "9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d", "productName": "Cashmere scarf", "variations": { "Colour": "Black" } } ], "deliveryCountry": "DE", "urgent": false, "receivedOutsideActiveHours": false, "shipments": [{ "carrier": "DHL", "trackingReference": "00340434161234567890", "trackingUrl": "https://…" }], "cancellationReason": null, "createdAt": "2026-10-05T09:12:44Z", "updatedAt": "2026-10-06T14:02:10Z"}| Field | Meaning |
|---|---|
reference |
Luxorr’s readable reference. Use it when you contact Luxorr. |
version |
0 when placed; increases with every change. Send it as expectedVersion to correct delivery details. |
externalOrderId |
Your order ID; null for orders not placed through the API. |
externalRecipientId, externalCampaignRef, externalActionRef |
Your references. An order from a personal-link claim carries the link’s externalRecipientId. |
source |
API, PERSONAL_LINK (claimed link), WORKSPACE (your team) or BACKOFFICE (Luxorr). |
campaignId, selection |
Campaign order: campaignId set, selection: null. Claim from a link sent with {brandId, productIds}: campaignId: null, selection: "CATALOG". Catalog or custom order: both null. |
items |
One item. kind is CATALOG or CUSTOM. |
shipments |
Empty until dispatch, then carrier and tracking. |
receivedOutsideActiveHours |
Received outside Luxorr’s working hours. Work starts at the next opening. |
The order contains the delivery country only: no name, email, phone or street.
Correct the delivery details
Section titled “Correct the delivery details”POST /orders/{orderId}:update-delivery changes the recipient, the courier comment or urgency while the order is PROCESSING. Send only the changed fields:
{ "expectedVersion": 3, "recipient": { "address": { "street": "Friedrichstraße 43" } }, "courierComment": "Leave with the concierge"}expectedVersionis required. Omittedrecipient,courierCommentandurgentkeep their values.recipientis merged with the stored one, field by field, includingrecipient.address. An omitted ornullfield keeps its value. The example changes only the street.- Fields cannot be cleared. An empty
name,phone,street,cityorcountryanswersrequired. An emptyemailorpostalCodekeeps the stored value. - The merged recipient is validated like a new order: international phone format and a postal code where required. Errors answer
400 invalid_requeston therecipient.*field, even if the invalid value is a stored one. Send that field to fix it. - Orders from a personal-link claim hold the details the player entered. You can still change one field, such as the phone.
- No change: a request that changes nothing answers
200with the order, no newversionand no event. It is still refused ifexpectedVersionis stale or the order is notPROCESSING. - Country. A different
address.countryanswers422 country_change_not_allowed. Cancel and place a new order, or send an order message. - Status. Outside
PROCESSING:409 not_editablewithcurrentStatus. - Stale version:
409 version_conflict. Read the order again and retry.
Cancel
Section titled “Cancel”POST /orders/{orderId}:cancel with {"reason": "Player self-excluded"} cancels a REQUESTED or PROCESSING order and releases its stock. An already CANCELLED order answers 200 unchanged: the first reason stays and no event is sent. A SENT or COMPLETED order answers 409 not_cancellable. See cancelling before and after dispatch.
Messages
Section titled “Messages”Each order has a conversation between Luxorr, your team and your keys.
GET /orders/{orderId}/messagesreturns messages oldest first, as a plain array:[{id, author: {kind, name}, body, createdAt}]. Luxorr internal notes are not included.POST /orders/{orderId}/messageswith{"body": "…"}(max 4,000 characters) posts as the key. Response:201 {id}.
author.kind is API_KEY, TENANT_ACCOUNT (your team), TENANT_CLIENT, BACKOFFICE_ACCOUNT (shown as “Luxorr”) or SYSTEM.
Messages from others also arrive as order.message_posted events. Use messages for requests the API does not cover: changes after dispatch, returns, questions about custom requests.