Skip to content

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 same Idempotency-Key returns the same order with Idempotent-Replayed: true. It isn’t a second order.
  • A refusal creates no order. If the API returns a 4xx code, deduct nothing. After a timeout or a 5xx code, retry with the same key and body, and deduct when a 201 arrives.
  • Refund once per orderId. Store the IDs of the orders you refunded, and skip any that you already have. Also deduplicate events by id, because a delivery can repeat.
  • Refund on order.cancelled only. order.sent and order.completed never move coins. Luxorr sends order.cancelled at most once per order.
  • The order keeps its price. Refund the coinPrice that the order carries, in its currency, not the store’s current price.

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.

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-DE
Authorization: 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 country under the brand, in the order your team set. A section with no such gift is left out.
  • coinPrice is a decimal string in the brand’s currency for 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. name and each value are the values to send in the order. label is for display, translated with locale.
  • combinations are the option sets a player can pick. Offer only those. A gift with no dimensions has dimensions: [], and you order it with variations: {}.
  • Images. The store has a banner. Each gift has a thumbnail, its main image, and a gallery, 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[].

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-orders
Authorization: Bearer lxr_live_…
Idempotency-Key: shop-order-58213
Content-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",
"email": "[email protected]",
"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.approvalRequired in GET /me), the order starts as REQUESTED and waits for your team. Otherwise, it starts as PROCESSING. A key can’t approve orders. An approval sends order.approved. If your team rejects the order, it’s canceled and sends order.cancelled.
  • Cancellation. You can’t cancel a store order through the API. To cancel one, ask your team. You then receive order.cancelled and refund the coins.
  • The order keeps its price and currency, whatever the store charges later.
  • Store the order id with your own order and the player. Orders placed through the API appear in the Luxorr workspace, labeled “via API” with the key’s name.

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.

You can read orders in two ways:

  • GET /reward-store-orders/{orderId} reads one order.
  • GET /reward-store-orders lists orders, newest first. You can filter by brandId, storeId, and status.

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.

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.
  • data is the RewardStoreOrder, the same as GET /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.

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.