Rewards stores
A rewards store is a list of gifts that your team sets up for one or more of your brands. For each brand, the store has a bonus currency, such as “Gold Coins”, and a price for every gift in the player’s country. You show the store on your site and hold each player’s balance. Luxorr takes the order, then buys, packs, and delivers the gift.
Requires: a key with the REWARD_STORES scope, and a webhook URL or a reader of the event feed.
Luxorr states the prices. It keeps no balance and holds no coins. Your system moves the player’s coins at exactly two moments:
| When | You |
|---|---|
POST /reward-store-orders returns 201 |
Deduct the order’s coinPrice from the player’s balance. |
order.cancelled arrives for an orderId you haven’t refunded |
Refund that order’s coinPrice to the player, once. |
| Any other response or event | Do nothing. |
Follow these rules:
- Deduct once per
orderId. A retry with the sameIdempotency-Keyreturns the same order withIdempotent-Replayed: true. It isn’t a second order. - A refusal creates no order. If the API returns a
4xxcode, deduct nothing. After a timeout or a5xxcode, retry with the same key and body, and deduct when a201arrives. - Refund once per
orderId. Store the IDs of the orders you refunded, and skip any that you already have. Also deduplicate events byid, because a delivery can repeat. - Refund on
order.cancelledonly.order.sentandorder.completednever move coins. Luxorr sendsorder.cancelledat most once per order. - The order keeps its price. Refund the
coinPricethat the order carries, in itscurrency, not the store’s current price.
List the stores
Section titled “List the stores”GET /reward-stores lists the stores your key can use, sorted by name. A listed store is SCHEDULED or LIVE and serves at least one of your key’s brands. You can filter by brandId.
{ "items": [ { "id": "4b3a2918-0716-4f5e-8d4c-3b2a19080716", "name": "VIP Rewards", "brandIds": ["0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b"], "phase": "LIVE", "startsAt": "2026-10-01T00:00:00Z", "endsAt": null } ], "nextCursor": null}The following table describes the fields:
| Field | Meaning |
|---|---|
brandIds |
The store’s brands that your key can use. |
phase |
SCHEDULED: you can read the store, but it opens at startsAt and takes no orders yet. LIVE: the store takes orders. |
startsAt, endsAt |
When the store opens and closes, or null for no limit. |
If a store has ended or your team withdrew it, the store leaves the list, and reading it returns 404 not_found.
Read the store for a player
Section titled “Read the store for a player”GET /reward-stores/{storeId}?brandId=…&country=…&locale=… returns the store as a player of one brand in one country sees it. It takes the following parameters:
| Parameter | Rules |
|---|---|
brandId |
Required. One of the store’s brandIds. |
country |
Required. The player’s delivery country, as an ISO 3166-1 alpha-2 code such as DE. |
locale |
Optional. One of tenant.locales in GET /me. The default is en-US. Gift names, descriptions, and option labels, section names, and the currency are translated where your team added a translation. The store’s own name and description appear as your team wrote them. |
GET /v1/reward-stores/4b3a2918-0716-4f5e-8d4c-3b2a19080716?brandId=0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b&country=DE&locale=de-DEAuthorization: Bearer lxr_live_…{ "id": "4b3a2918-0716-4f5e-8d4c-3b2a19080716", "name": "VIP Rewards", "description": "Gifts for our VIP players.", "phase": "LIVE", "startsAt": "2026-10-01T00:00:00Z", "endsAt": null, "banner": { "url": "https://images.example/stores/vip-rewards/banner.jpg", "variants": [ { "width": 640, "url": "https://images.example/stores/vip-rewards/banner-640.jpg" }, { "width": 1280, "url": "https://images.example/stores/vip-rewards/banner-1280.jpg" } ] }, "brand": { "id": "0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b", "name": "Example Brand" }, "currency": { "name": "Goldmünzen", "symbol": "GC" }, "country": "DE", "locale": "de-DE", "sections": [ { "id": "8c7b6a59-4837-4261-9f0e-1d2c3b4a5968", "name": "Mode", "items": [ { "productId": "2f1e0d9c-8b7a-4695-a483-72615f4e3d2c", "name": "Kapuzenpullover", "description": "Baumwolle, mit Stickerei.", "thumbnail": { "url": "https://images.example/gifts/hoodie.jpg", "variants": [{ "width": 320, "url": "https://images.example/gifts/hoodie-320.jpg" }] }, "gallery": [], "dimensions": [ { "name": "Size", "label": "Größe", "values": [ { "value": "M", "label": "M" }, { "value": "L", "label": "L" } ] }, { "name": "Color", "label": "Farbe", "values": [ { "value": "Black", "label": "Schwarz" }, { "value": "Gray", "label": "Grau" } ] } ], "combinations": [ { "variations": { "Size": "M", "Color": "Black" } }, { "variations": { "Size": "L", "Color": "Black" } }, { "variations": { "Size": "L", "Color": "Gray" } } ], "coinPrice": "450" }, { "productId": "6d5c4b3a-2918-4e7d-8c6b-5a4938271605", "name": "Thermobecher", "thumbnail": null, "gallery": [], "dimensions": [], "combinations": [], "coinPrice": "120.5" } ] } ]}Keep the following in mind when you show the store:
- Text. Every
description, the store’s and each gift’s, is plain text with no markup. Keep its line breaks. - Only what the player can order. The sections hold the gifts that can be delivered to
countryunder the brand, in the order your team set. A section with no such gift is left out. coinPriceis a decimal string in the brand’scurrencyfor this store. Parse it as a decimal, never as a float, and send it back unchanged when the player orders. One price covers every combination of the gift.- Dimensions are a gift’s options, such as size and color.
nameand eachvalueare the values to send in the order.labelis for display, translated withlocale. combinationsare the option sets a player can pick. Offer only those. A gift with no dimensions hasdimensions: [], and you order it withvariations: {}.- Images. The store has a
banner. Each gift has athumbnail, its main image, and agallery, its further images without the thumbnail. For more information, see Images. - Prices can change at any time. Read the store again before you show it. The order checks the price.
If brandId isn’t one of the store’s brands, or your key can’t use it, the API returns 404 not_found. If brandId, country, or locale is malformed, the API returns 400 invalid_request with errors[].
Place an order
Section titled “Place an order”POST /reward-store-orders orders one gift for one player. Each order holds one gift; there is no cart. The store takes orders only while it’s LIVE.
Send it with an Idempotency-Key built from your own order, such as your shop’s order ID:
POST /v1/reward-store-ordersAuthorization: Bearer lxr_live_…Idempotency-Key: shop-order-58213Content-Type: application/json{ "storeId": "4b3a2918-0716-4f5e-8d4c-3b2a19080716", "brandId": "0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b", "productId": "2f1e0d9c-8b7a-4695-a483-72615f4e3d2c", "variations": { "Size": "L", "Color": "Black" }, "coinPrice": "450", "locale": "de-DE", "recipient": { "name": "Alice Smith", "phone": "+4915112345678", "address": { "street": "Beispielstraße 12", "city": "Berlin", "postalCode": "10115", "country": "DE" } }, "additionalDetails": "Please ring twice.", "customFields": [ { "fieldId": "5c4b3a29-1807-4f6e-8d5c-4b3a29180746", "textValue": "player-123" } ]}The following table describes each field:
| Field | Rules |
|---|---|
storeId |
Required. A LIVE store that your key can use. |
brandId |
Required. The brand you read the store for. |
productId |
Required. A gift that the store offers for that brand and the delivery country. |
variations |
Required. One of the gift’s combinations, by dimension name and value. Use {} if the gift has no dimensions. |
coinPrice |
Required. The price the player saw, exactly as the store returned it. |
locale |
Optional. The player’s language: one of tenant.locales in GET /me. The default is en-US. Luxorr writes the player’s status emails and the order’s productName in this language. |
recipient.name |
Required. The person the gift is for and ships to. |
recipient.email |
Optional, up to 255 characters. Luxorr sends order status updates to it, in the order’s locale. |
recipient.phone |
Required. An E.164 number, such as +4915112345678. |
recipient.address.street, .city |
Required. |
recipient.address.postalCode |
Required if the country uses postal codes. |
recipient.address.country |
Required. An ISO 3166-1 alpha-2 code. Luxorr checks the price and availability for the delivery country, so read the store with the same country. |
additionalDetails |
Optional. Delivery notes, up to 2,000 characters. |
customFields |
Optional. Answers to your own order fields, as on a personal link. A required field must have an answer. |
The API returns 201 with the order. Deduct the coins now.
{ "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "reference": "G-0000004217", "storeId": "4b3a2918-0716-4f5e-8d4c-3b2a19080716", "brandId": "0a6f2e1c-3b5d-4e7f-8a9b-0c1d2e3f4a5b", "productId": "2f1e0d9c-8b7a-4695-a483-72615f4e3d2c", "productName": "Kapuzenpullover", "variations": { "Size": "L", "Color": "Black" }, "status": "PROCESSING", "coinPrice": "450", "currency": { "name": "Gold Coins", "symbol": "GC" }, "deliveryCountry": "DE", "customFields": [ { "fieldId": "5c4b3a29-1807-4f6e-8d5c-4b3a29180746", "textValue": "player-123", "checked": null } ], "shipment": null, "createdAt": "2026-10-16T10:04:31.208114Z", "updatedAt": "2026-10-16T10:04:31.208114Z"}After you place an order, keep the following in mind:
- Approval. If your account requires approval (
tenant.approvalRequiredinGET /me), the order starts asREQUESTEDand waits for your team. Otherwise, it starts asPROCESSING. A key can’t approve orders. An approval sendsorder.approved. If your team rejects the order, it’s canceled and sendsorder.cancelled. - Cancellation. You can’t cancel a store order through the API. To cancel one, ask your team. You then receive
order.cancelledand refund the coins. - The order keeps its price and currency, whatever the store charges later.
- Store the order
idwith your own order and the player. Orders placed through the API appear in the Luxorr workspace, labeled “via API” with the key’s name.
When the order is refused
Section titled “When the order is refused”If the API refuses the order, nothing is created and no stock is taken. Deduct nothing. The following table lists the refusals:
| Response | What to do |
|---|---|
409 price_changed |
The price for the delivery country changed. The response carries the current coinPrice. Show the player the new price, and if they accept, send the order with the new price. |
422 gift_not_available |
The gift can’t be ordered for this brand and delivery country. reason says why. Remove the gift from what the player sees. |
409 out_of_stock |
The chosen combination is out of stock. Offer another combination or gift. |
422 reward_store_not_live |
The store hasn’t opened yet or has ended. |
422 reward_store_not_found |
Your key can’t use this store. |
422 brand_not_found |
The brand isn’t one of the store’s, or your key can’t use it. |
400 invalid_request |
A field is missing or invalid: a recipient field, a postal code the country needs, variations that aren’t one of the gift’s combinations, or a missing required custom field. errors[] names each field. |
422 idempotency_key_reused |
An order was already placed under this key with a different body. |
{ "type": "https://docs.luxorr.io/errors#price_changed", "title": "Price changed", "status": 409, "code": "price_changed", "detail": "The coin price for this gift is now 480", "coinPrice": "480"}The following table describes each reason value on gift_not_available:
| Reason | Meaning |
|---|---|
NOT_IN_STORE |
The store doesn’t hold the gift. |
NOT_PUBLISHED |
The gift was withdrawn from the catalog. |
NOT_USABLE_BY_BRAND |
The gift can’t be offered under this brand. |
NOT_SOLD_IN_COUNTRY |
The gift isn’t delivered to the delivery country. |
NOT_AVAILABLE_NOW |
The gift can’t be ordered right now. Try again later. |
NO_SELECTABLE_COMBINATION |
No combination of the gift’s options can be picked. |
A refusal isn’t stored under the Idempotency-Key, so you can use the same key again after you fix the cause. For more information, see Idempotency.
Follow the order
Section titled “Follow the order”You can read orders in two ways:
GET /reward-store-orders/{orderId}reads one order.GET /reward-store-orderslists orders, newest first. You can filter bybrandId,storeId, andstatus.
Your key reads only the store orders it placed. For any other order, the API returns 404 not_found.
productName is in the order’s locale, or in en-US if the gift has no translation. variations keep the values you sent. To show them, use the labels from the store read.
The following table describes each order status:
| Status | Meaning |
|---|---|
REQUESTED |
Waiting for your team’s approval. |
PROCESSING |
Luxorr is preparing the gift. |
SENT |
The gift shipped. shipment carries carrier, trackingReference, and trackingUrl where known. |
COMPLETED |
The gift was delivered. |
CANCELLED |
Your team or Luxorr canceled the order. |
No personal data comes back. An order never returns the player’s name, email, phone number, or address. It returns deliveryCountry and the customFields answers. To match an order to the player, put your player ID in one of your fields.
Events
Section titled “Events”The following table lists the order events:
| Event | Sent when | You |
|---|---|---|
order.approved |
Your team approved an order that started as REQUESTED. |
Show the gift as being prepared. Coins don’t move. |
order.cancelled |
The order was canceled, including a rejection while it was REQUESTED. |
Refund coinPrice once for this orderId. |
order.sent |
The order shipped. data.shipment carries the tracking. |
Show the tracking to the player. Coins don’t move. |
order.completed |
The order was delivered. | Nothing is required. Coins don’t move. |
Keep the following in mind:
- Each event is sent at most once per order. Other changes, such as an edit, send no event. To see the current status, read the order.
datais the RewardStoreOrder, the same asGET /reward-store-orders/{orderId}returns.- Only the key that placed the order receives its events, as webhooks and in its event feed. Your other keys never receive them, so every refund belongs to an order your key charged.
Images
Section titled “Images”Image addresses come only from store reads: banner, thumbnail, and gallery. Each image has a url, its widest version, and variants by width, narrowest first, for srcset.
Follow these rules:
- Use each address as given. It’s a complete URL that opens with no API key, so you can put it straight into an
<img>tag. - Cache freely. An image address is cacheable. A replaced image gets a new address.
- Stay within the limit. Image addresses allow 600 requests a minute per client IP address, separate from your key’s limit.
For more information, see Webhooks and events.

