> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kintra.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Rewards and redemptions

> The lifecycle from a reward you publish to a redemption that has settled, with the refusals along the way and what each record means.

A reward is something a customer can spend XP on, created under your tenant with a title and a cost. A redemption is one customer spending that cost on it. This page follows the path from the first to the second. {/* trace: T-06, T-13 */}

## Creating a reward

`POST /gateway/rewards` takes a title and an XP cost, and optionally a description, freeform metadata, and the id of a tier that gates it. The cost is a whole number of at least one point. {/* trace: T-06 */}

The gate is the `min_tier_id` field. It has to name a tier of your own tenant, and one that does not returns 404 `TIER_NOT_FOUND`; leaving it out or sending it empty means the reward is open to every customer. {/* trace: T-11 */}

A reward has two statuses, `initialized` and `published`. A create with no status stores `initialized`, and a create may set `published` straight away. {/* trace: T-07 */}

## Publishing freezes it

Publishing is the point of no return, and it is worth getting the reward right before you cross it. An update to a published reward returns 400 `REWARD_PUBLISHED` unless the only field in the body is `is_active`, and a delete of a published reward returns the same code. {/* trace: T-08 */}

So `is_active` is the one control you keep. Setting it to false takes a published reward out of circulation without removing it, which is what you use instead of a delete. {/* trace: T-09 */}

Before publication a delete is available and is a soft delete: the reward stops being visible rather than being erased, so redemptions that already reference it still read back. {/* trace: T-09 */}

Creating a reward and updating one both require `X-Idempotency-Key`. Deleting one does not, even though the generated reference declares the header: the middleware that enforces it exempts every DELETE. {/* trace: CreateReward, UpdateReward, M-4 */}

## Redeeming

`POST /gateway/xp/redeem` takes the customer and the reward. The cost is not in the request: it comes from the reward, which is what makes a published reward's price the one a customer actually paid. {/* trace: T-13 */}

The refusals run in a fixed order, and knowing it saves you guessing which one a client hit: {/* trace: T-12 */}

* 404 `USER_NOT_FOUND` when the customer is not a member of your tenant. {/* trace: T-12 */}
* 400 `XP_USER_LOCKED` when that customer's XP balance is locked, and 400 `USER_LOCKED` when their account is not `active` or `reinstated`. {/* trace: T-12 */}
* 404 `REWARD_NOT_FOUND` when the reward is not one of yours, and 400 `REWARD_INACTIVE` when it is yours but is unpublished or has been deactivated. {/* trace: T-10, T-12 */}
* 403 `REWARD_TIER_LOCKED` when the reward is gated to a tier the customer's balance has not reached. The cost is never compared when this one fires. {/* trace: A-30 */}
* 400 `XP_INSUFFICIENT_BALANCE` when the customer has reached the tier and still cannot afford the reward. The response names what was required and what was available. {/* trace: A-10 */}

Handle the last two separately. A reward whose tier gate sits at or above its own cost answers a short balance with the 403 and never with the 400. {/* trace: A-30 */}

The cost check reads a snapshot of the indexed balance and reserves nothing, so two redeems for one customer issued before the first burn is indexed can both pass it. Serialize a customer's redeems rather than firing them together: a redeem the API accepted against XP another redeem had already spent still returns 200, and the surplus one reaches `failed` after the fact. {/* trace: M-7 */}

Two further refusals can arrive in the same sequence, and both are about the environment rather than the request: 503 `XP_SERVICE_NOT_AVAILABLE` when the XP service is unavailable, and 404 `ONCHAIN_ASSET_NOT_FOUND` when no XP asset is configured for the chain. Both mean retry rather than change the request. {/* trace: T-12 */}

A redeem requires `X-Idempotency-Key`, and replaying a key returns the stored response rather than spending twice. {/* trace: RedeemReward, C-25 */}

## What an accepted redemption writes

Two records, in one database transaction. The first is the internal transaction an earn also produces, at status `initialized` and carrying the amount as an unsigned magnitude with `xp_redeem` as its type. The second is the redemption record, which carries the reward, the XP spent, and its own status. {/* trace: T-14, A-32 */}

The response is the internal transaction, and its `redemption_id` is how you get from one to the other. {/* trace: T-14 */}

Read redemptions back on `GET /gateway/redemptions` and `GET /gateway/redemptions/{redemption_id}`, each carrying `xp_spent`, a status, and a summary of the reward that was redeemed. {/* trace: T-17 */}

## What the statuses mean

A redemption record declares five statuses: `initialized`, `pending`, `succeeded`, `failed` and `canceled`. Three are written today, `initialized` on acceptance and then `succeeded` or `failed` from what becomes of the burn, including the case where the burn is never attempted, so a client switching over all five has two branches nothing reaches yet. {/* trace: T-15 */}

One of those three does not mean what its name suggests. A redemption reaches `succeeded` when the burn is broadcast to the chain, not when the chain confirms it, and a burn that then reverts moves the record to `failed`. So `succeeded` is not a settled balance. {/* trace: T-16 */}

Follow the internal transaction when you need to know the spend has landed: it moves to `pending` on the same broadcast and to `completed` or `failed` from the chain receipt, and the chain transaction under it carries the hash and its own status. {/* trace: A-13, A-14, A-15 */}

A redemption can also reach `failed` with no chain transaction under it at all. When the burn is given up on before it is broadcast, the record sits at `initialized` for the whole retry budget and then flips to `failed` with nothing linked, so an empty list of chain transactions is not a reliable sign that work is still in flight. {/* trace: T-15, T-16 */}

The balance follows the same clock as an earn. The cost leaves the balance when the burn is indexed, which is after the redeem call has already answered you. {/* trace: A-17, A-26 */}

## Where to go next

* [XP and the ledger](/concepts/xp-and-the-ledger) for what a balance is and what an earn does.
* [Tiers](/concepts/tiers) for the threshold a gated reward is measured against.
* [On-chain XP](/concepts/onchain-xp) for what the burn behind a redemption does.
* [Request conventions](/conventions) for the error envelope every refusal above arrives in.
