Skip to content

Orders

Order endpoints need the ORDERS scope.

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" } }
],
"client": { "name": "Wendy Player", "email": "[email protected]" },
"recipient": {
"name": "Wendy Player",
"email": "[email protected]",
"phone": "+4930123456",
"address": { "street": "Unter den Linden 1", "city": "Berlin", "postalCode": "10117", "country": "DE" }
},
"managerEmail": "[email protected]",
"courierComment": "Ring twice",
"urgent": false
}
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.

  • recipient.address.postalCode is 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_request with {"field": "recipient.address.postalCode", "code": "required", "message": "recipient.address.postalCode is required for DE"}.
  • recipient.phone uses international format: +, country code, number, such as +4930123456. Letters, a missing + or country code, or a wrong length answer invalid on recipient.phone.
  • street max 255 characters, city max 100, country ISO 3166-1 alpha-2.
  • Approval. When the tenant requires approval, the order starts REQUESTED until a team member approves it in the workspace. Keys cannot approve. Otherwise it starts PROCESSING.
  • Owner. A team member named in managerEmail owns the order.
  • Workspace. The order appears in the workspace, labelled “via API” with the key name.

See order statuses.

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 as recipient.phone, or recipient.address.postalCode where required;
  • too_long: longer than allowed, such as externalOrderId over 128 characters;
  • invalid: malformed or not allowed, such as an impossible recipient.phone, a wrong JSON type ("urgent": "yes" gives urgent must be true or false), brandId on brand_campaign_mismatch, or managerEmail on manager_not_found.

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 brandId of “Second Brand” gets 422 brand_not_found. Reading an order of “Second Brand” answers 404 not_found.
  • GET /orders/{orderId} reads one order by Luxorr ID.
  • GET /orders?externalOrderId=ORD-1001 finds 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.

  1. Call GET /orders?updatedSince=<instant>&limit=100 and follow nextCursor until it is null.
  2. Replace your copy of each order. Keep the largest updatedAt across all pages.
  3. 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.

{
"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.

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"
}
  • expectedVersion is required. Omitted recipient, courierComment and urgent keep their values.
  • recipient is merged with the stored one, field by field, including recipient.address. An omitted or null field keeps its value. The example changes only the street.
  • Fields cannot be cleared. An empty name, phone, street, city or country answers required. An empty email or postalCode keeps 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_request on the recipient.* 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 200 with the order, no new version and no event. It is still refused if expectedVersion is stale or the order is not PROCESSING.
  • Country. A different address.country answers 422 country_change_not_allowed. Cancel and place a new order, or send an order message.
  • Status. Outside PROCESSING: 409 not_editable with currentStatus.
  • Stale version: 409 version_conflict. Read the order again and retry.

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.

Each order has a conversation between Luxorr, your team and your keys.

  • GET /orders/{orderId}/messages returns messages oldest first, as a plain array: [{id, author: {kind, name}, body, createdAt}]. Luxorr internal notes are not included.
  • POST /orders/{orderId}/messages with {"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.