> ## 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.

# XP and the ledger

> XP as a ledger of signed integer movements, what a balance is, and what an earn and a redemption actually do.

XP is a whole number of points a customer holds under your tenant. Every movement of it is recorded as one transaction, so the ledger is the list of movements and a balance is what those movements have settled to. Two operations move it: an earn credits XP to a customer, and a redemption spends it on a reward you published. {/* trace: A-06, A-07, A-26 */}

## Amounts are signed integers

Model an XP amount as a signed integer in your own code, and never as a floating point value. {/* trace: A-06, A-32 */}

Two reasons, both practical. The first is direction, and the sign is yours to apply rather than something the API hands you: `amount` comes back as an unsigned magnitude on an earn and on a redemption alike, and `type` is the field that says which way the movement went. `xp_earn` is a credit and `xp_redeem` is a debit, so summing the amounts without reading `type` counts a spend as a credit and overstates the history by twice what was redeemed. {/* trace: A-32 */}

Two more values exist on `type`, `xp_bonus` and `xp_admin_adjust`. Nothing writes either today, so a switch over the four has two branches with no direction to assign yet rather than a miscount waiting to happen. {/* trace: A-32 */}

The second reason is that the values are exact whole numbers with no smallest fractional unit, because the XP token declares no decimal places, so there is no such thing as part of a point. {/* trace: A-06 */}

One thing to filter before you add anything up: a transaction at status `failed` is not a movement. The transaction list applies no status filter unless you ask for one, so a read with no filter hands you `initialized`, `pending` and `failed` rows next to the settled ones, and a naive sum counts a failed redemption as a spend. {/* trace: P-10, A-14 */}

Every amount the API returns is a string, including the balance, and that is deliberate. A balance can grow past the range a floating point number represents exactly, and a client that parses it as one starts losing low digits silently rather than failing. Parse the amounts you read into whatever your language calls a big integer, or keep them as strings and hand them to an integer type at the edge. {/* trace: A-06 */}

The request direction is not the same, so do not send a string back. `POST /gateway/xp/earn` declares `amount` as a JSON number: send it unquoted, as a whole number inside the range a JSON number represents exactly. {/* trace: A-06 */}

## What a balance is

A balance is the XP held on chain for that customer, summed across the wallet addresses Kintra manages for them. It is not a running total that the API increments as writes arrive: the platform reads it back from the token and reports what the token answered. {/* trace: A-08, A-09 */}

Read one with `GET /gateway/customers/{userId}/xp/balance`, which returns it as `data.balance`. That is the number to show a customer and the number to check before you offer them a redemption. {/* trace: P-09 */}

The practical consequence of a balance coming from the chain is timing, and it is the one thing about XP worth internalising before you write the integration: {/* trace: A-17 */}

**A write does not move the balance.** An earn or a redemption is accepted, recorded, and queued for the chain. The balance changes when that chain work settles and is picked up, which is after the response has already come back to you. {/* trace: A-07, A-17 */}

**So a read taken immediately after a write can still answer the old number.** That is not an error and retrying the write is the wrong response, because a second earn grants a second time. Treat the response to the write as the receipt that it was accepted, and read the balance when you need to display it. {/* trace: A-17, C-25 */}

## What an earn does

`POST /gateway/xp/earn` credits an amount of XP to a customer, with optional notes recorded against the entry. Call it when the customer does the thing you reward. {/* trace: A-07, P-11 */}

The response carries the transaction the credit created, at status `initialized`, and no balance. A transaction moves through four statuses: `initialized` when it is recorded, `pending` once the chain work has been submitted, then `completed` or `failed` from what the chain reported. Read it back on the customer's transaction list, or by its own id, to follow it. {/* trace: A-07, A-12, A-13, A-14, A-15 */}

An earn requires an idempotency key. A replay of the same key returns the stored response instead of crediting twice, which is exactly the behaviour you want when a request times out and you do not know whether it landed. {/* trace: EarnXp, C-25 */}

## What a redemption does

`POST /gateway/xp/redeem` spends a customer's XP on a reward. It takes the customer and the reward, and the cost comes from the reward rather than from your request. {/* trace: A-10 */}

It can be refused in two different ways, and a client that handles only one of them will look broken: {/* trace: A-30, A-10 */}

* 403 with `REWARD_TIER_LOCKED` when the reward is gated to a tier the customer has not reached. This check runs first, and the cost is never compared when it fails. {/* trace: A-30 */}
* 400 with `XP_INSUFFICIENT_BALANCE` when the customer has reached the tier but cannot afford the reward. The response names what was required and what was available. {/* trace: A-10 */}

Handle both. 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 */}

On success it records a transaction the same way an earn does, and the spend leaves the balance when the chain work settles, on the same delay and for the same reason. {/* trace: A-17, A-26 */}

A balance can also be held: setting its status to `locked` marks it, and setting it back to `active` releases it. That is a state you manage, and the customer list reports it. {/* trace: P-07 */}

## Where to go next

* [Customers](/concepts/customers) for the record a balance belongs to.
* [Tenants](/concepts/tenants) for why a balance is only ever yours to read.
* [Quickstart](/quickstart) to award XP and read the balance back in two signed requests.
* [Request conventions](/conventions) for the error envelope behind both refusals above.
