Skip to main content
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.

Amounts are signed integers

Model an XP amount as a signed integer in your own code, and never as a floating point value. 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. 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. 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. 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. 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. 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.

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

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

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. It can be refused in two different ways, and a client that handles only one of them will look broken:
  • 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.
  • 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.
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. 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. 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.

Where to go next

  • Customers for the record a balance belongs to.
  • Tenants for why a balance is only ever yours to read.
  • Quickstart to award XP and read the balance back in two signed requests.
  • Request conventions for the error envelope behind both refusals above.