Skip to content

Wallets and Points

Every external user in a project has one wallet. A wallet holds any number of asset types (points, coffee_stamps, credits, …), each with its own balance. Underneath, each balance is the sum of asset entries (lots created by earns, each with an optional expiry), and every change is recorded as a transaction in an append-only ledger.

There is no registry of asset types. The first earn of "assetType": "stars" creates that balance; a typo such as "point" silently creates a second, separate balance. Asset types are not converted into each other or into money. Pick names once, use them everywhere (API calls, rule actions, conditions), and treat them as case-sensitive.

The wallet also does not check that the customer profile exists. Writing to an unknown user ID creates a new, empty wallet for that ID. Resolve the customer ID before writing to a wallet.

LayerEndpointWhat it answersImportant distinction
BalanceGET /projects/{projectId}/users/{userId}/wallet“How many points does the customer have now?”Also returns lifetime earned/spent/expired and the next expiry date per asset type
Asset entriesGET …/wallet/assets“Which lots make up the balance and when do they expire?”Only lots with something left in them; fully spent or expired lots are removed
TransactionsGET …/wallet/transactions“How did the balance change, and why?”Permanent ledger: earn, spend, expire, adjust

The balance response is keyed by asset type:

{
"success": true,
"data": {
"assets": {
"points": {
"assetType": "points",
"balance": 1250,
"lifetimeEarned": 2000,
"lifetimeSpent": 700,
"lifetimeExpired": 50,
"nextExpiration": "2026-12-31T23:59:59.000Z"
}
}
}
}

Read a balance from the REST API as data.assets.points.balance. An asset type the customer has never earned is absent from assets; treat that as 0. Lifetime totals count earn, spend and expire transactions only. adjust transactions from modify are not included.

Inside rules, a wallet resource is reshaped so both {{ $('wallet').points.balance }} and {{ $('wallet').balances.points }} work. See conditions and resources.

OperationRequest fieldEffectChoose it when
Earnamount (> 0)Creates a new asset entry, optionally with expiresAt and tags; records an earn transactionAwarding something: purchase points, bonuses, referral rewards
Spendamount (> 0)Consumes existing entries, first expiring first; records one spend transactionRedeeming or deducting a known amount
ModifytargetAmount (≥ 0)Computes delta = target − current balance. A positive delta creates one entry; a negative delta consumes entries like a spend. Records one adjust transaction with the signed deltaCorrecting a balance to an exact value: migrations, support corrections, syncing from another system

Never use modify to award or deduct a relative amount. “Give 100 points” is an earn. “Set to 100 points” is a modify. Running a modify twice with the same target is harmless. Running an earn twice credits twice.

Modify with a target equal to the current balance does nothing and returns "transaction": null, "delta": 0.

Spend (and negative modify) consumes entries first-expiring-first-out (FEFO): entries with the earliest expiresAt go first, and entries with no expiry go last. A spend that needs more than the available balance fails as a whole with HTTP 400, error code 2601 (“Insufficient balance”). Nothing is partially deducted. A wallet can never go negative.

  • expiresAt is set per earn (or per positive modify) and must be a future UTC timestamp such as 2026-12-31T23:59:59Z. A past date is rejected with error 2600.
  • Without expiresAt, the entry never expires.
  • Expired entries are removed and an expire transaction is written for the remaining amount. This runs on a schedule and also before every wallet read or write, so a balance you read never includes expired points.
  • Rule reward actions can set expiry relatively (expiresIn + expiresInUnit, e.g. 365 + days) or absolutely (expirationDate). See the loyalty program guide.

A project can opt in to wallet events: wallet_asset_earned, wallet_asset_spent, wallet_asset_modified, wallet_asset_expired. To opt in, register a resource of type event whose key is the event name. The event is emitted for the wallet’s owner. Its payload carries assetType, balance (the balance after the change) and transactionId, plus amount for earned/spent/expired or delta and previousBalance for modified. Earned, spent and modified events also carry referenceId and referenceType. Use this for tier upgrades (“when the balance reaches 5000”) or notifications. Watch out for loops: a rule that earns on wallet_asset_earned triggers itself.

  • Amounts are plain numbers in the asset’s own unit. A point is not a cent. Keep amounts as integers; the API does not reject fractions.
  • Direct API writes and rule actions write to the same wallet. Before adding a direct award, check whether a rule already awards for the same business event.
  • referenceId is stored for traceability but is not checked for duplicates. See working with wallets for safe retries.