Skip to content

Working with Wallets

All wallet endpoints are scoped to one customer: /projects/{projectId}/users/{userId}/wallet…. Writes take effect right away and return the transaction plus the updated balances. Choosing between earn, spend and modify is covered in wallet concepts.

POST /projects/{projectId}/users/{userId}/wallet/earn

{
"assetType": "points",
"amount": 500,
"expiresAt": "2027-06-30T23:59:59Z",
"tags": ["campaign:summer"],
"referenceId": "order_10045",
"referenceType": "order",
"description": "Summer double points",
"metadata": { "channel": "pos" }
}

Only assetType and amount (> 0) are required. expiresAt must be a future UTC timestamp ending in Z. tags are copied onto the asset entry and can be used to filter entries. referenceId, referenceType, description and metadata are saved on the transaction (and the reference also on the entry) so you can trace it later.

The response data contains transaction, asset (the new entry) and wallet (all balances after the change).

POST /projects/{projectId}/users/{userId}/wallet/spend

{
"assetType": "points",
"amount": 300,
"referenceId": "reward_redemption_77",
"referenceType": "reward",
"description": "Free coffee"
}

Spend takes no expiresAt or tags. It consumes entries first-expiring-first-out. The response data contains transaction, allocations (how much came from each entry) and wallet. Discounts paid with points at checkout go through promotion redemptions, not this endpoint. See promotions.

POST /projects/{projectId}/users/{userId}/wallet/modify

{
"assetType": "points",
"targetAmount": 1000,
"description": "Balance migrated from legacy system",
"referenceId": "migration_2026_10"
}

targetAmount must be ≥ 0. Optional fields are the same as for earn; expiresAt and tags apply only when the balance goes up. The response data contains transaction (an adjust with the signed delta, or null when nothing changed), delta, previousBalance, newBalance, asset, allocations and wallet.

HTTPerror.codeMeaningWhat to do
4002601Insufficient balance (spend)Nothing was deducted. Show the current balance; do not retry
4002600Invalid input, e.g. expiresAt in the past or unparseableFix the request
400—Request validation failure (missing assetType, amount ≤ 0)Fix the request

Errors use the envelope { "success": false, "error": { "code": 2601, "message": "Insufficient balance" } }.

GET /projects/{projectId}/users/{userId}/wallet/transactions?assetType=points&type=earn&limit=50&offset=0
GET /projects/{projectId}/users/{userId}/wallet/assets?assetType=points&tag=campaign:summer&limit=50&offset=0

Both return { "items": [...], "total": n } with the newest first. limit is 1–100 (default 50). Use offset to page and total to know when to stop. type is one of earn, spend, expire, adjust. On a transaction, amount is positive for earn, spend and expire and signed for adjust. balanceAfter is the balance of that asset type right after the transaction. metadata comes back as a JSON-encoded string.

Wallet writes have no idempotency key. referenceId is stored but not checked for duplicates, so sending the same earn twice credits twice.

After a timeout or network error on a write:

  1. Do not resend straight away.
  2. List recent transactions (?assetType=…&type=earn&limit=20) and look for your referenceId.
  3. Resend only if it is not there.

Give every write a unique referenceId (order ID, redemption ID) so this check is reliable. If an exact final value is what you need, modify is naturally safe to retry.

  1. GET …/wallet and note data.assets.<assetType>.balance (missing means 0).
  2. Make the write and keep the returned transaction.id.
  3. GET …/wallet again: the balance changed by exactly the intended amount for the intended asset type.
  4. GET …/wallet/transactions?limit=5: the transaction has the expected type, amount, referenceId and balanceAfter.
  5. For an expiring earn, GET …/wallet/assets shows the entry with the expected expiresAt.

Check the userId in the path as carefully as the amount: a wrong ID silently creates a separate wallet.

  • Awards for business events (purchases, signups) belong in rules, so they happen automatically and are recorded in workflows. Use direct earn for one-off grants and support corrections. See the loyalty program guide.
  • The project API key can change any customer’s wallet. Never let a browser call these endpoints directly; your backend decides which userId the request is allowed to touch.