Skip to main content
Points are the spendable currency of a member’s wallet. They are minted by your backend, spent when a member redeems an offer, and refunded when a redemption is canceled. Every change is recorded as a transaction you can read back as a ledger. Two modes touch the wallet, and they use different credentials:
StepModeCredentialEffect on balance
MintServer-modeOrg-API-Key (backend only)Increases (credit)
SpendMember-modeMember token (app)Decreases (debit)
RefundMember-modeMember token (app)Increases (credit)
Read balanceMember-modeMember token (app)None
Read ledgerMember-modeMember token (app)None
Member-mode calls authenticate with the short-lived member token your backend mints (see Authentication), and the environment - sandbox or live - is selected by the key the token was minted from. Server-mode calls authenticate with the Org-API-Key directly.

Mint points (server-side)

Minting credits points into a member’s wallet. Your backend calls POST /v1/wallet/credit with the Org-API-Key. amount is an integer number of points, and an optional reason is stored on the resulting credit transaction.
Minting is server-side only. It requires the Org-API-Key and must run on your backend - never in an app or mobile binary. A member token calling POST /v1/wallet/credit is rejected with 403 FORBIDDEN. The Org-API-Key has full org authority; treat it as a secret.
externalUserID
string
required
Your stable identifier for the member to credit.
amount
integer
required
The number of points to mint. A positive integer.
reason
string
Optional free text stored on the credit transaction (for example "welcome bonus").
Send an Idempotency-Key header so a retried mint replays the original result rather than crediting twice. See Retries and idempotency.
curl -X POST https://api.expys.com/v1/wallet/credit \
  -H "Authorization: Bearer YOUR_ORG_API_KEY" \
  -H "Idempotency-Key: 0f3a9c2e-4b1d-4e6a-9c7b-1f2e3d4c5b6a" \
  -H "Content-Type: application/json" \
  -d '{
    "externalUserID": "user_123",
    "amount": 100,
    "reason": "welcome bonus"
  }'
The response is a CreditWalletResponse with the member’s new balance:
balance
number
required
The member’s wallet balance after the credit.
currency
object
required
The wallet currency, with name and symbol.
In the SDKs, minting is creditPoints(...), one of the server-mode methods that run with the Org-API-Key. The full server-mode flow - exchanging a member token, upserting a member, and crediting points - looks like this:
import { initialize } from "@expys/sdk";

const orgApiKey = process.env.EXPYS_ORG_API_KEY;
if (!orgApiKey) {
  throw new Error(
    "Set EXPYS_ORG_API_KEY (your secret Org-API-Key, e.g. expys_live_...). " +
      "Run this on a backend only, never in a client app.",
  );
}

// Construct the client with the machine credential as the token. Server-mode
// methods are guarded against member tokens client-side.
const expys = initialize({
  baseUrl: process.env.EXPYS_BASE_URL,
  environment: "sandbox",
  token: orgApiKey,
});

const externalUserID = process.env.EXPYS_EXTERNAL_USER_ID ?? "user_42";

async function main(): Promise<void> {
  // Mint a short-lived member token for your app to use (return this to the app,
  // never the Org-API-Key). Idempotent POST: a retry replays rather than re-mints.
  const grant = await expys.exchangeToken({ externalUserID });
  console.log(`minted member token expiring at ${grant.expiresAt}`);

  // Upsert the member's profile. PUT is idempotent by HTTP semantics (no key).
  const member = await expys.setMember(externalUserID, {
    displayName: "Ada Lovelace",
    tier: "gold",
  });
  console.log(`member ${member.externalUserID} is now tier=${member.tier}`);

  // Credit points to the member's wallet. Idempotent POST sends an Idempotency-Key.
  const credited = await expys.creditPoints({
    amount: 100,
    externalUserID,
    reason: "welcome bonus",
  });
  console.log(`new balance: ${credited.balance} ${credited.currency.symbol}`);

  // Register a webhook. The signingSecret is shown ONLY on creation - store it now.
  const webhook = await expys.createWebhook({
    events: ["redemption.created"],
    url: "https://example.com/expys/webhooks",
  });
  console.log(`webhook ${webhook.id} secret: ${webhook.signingSecret}`);

  // Org-wide analytics rollups.
  const summary = await expys.analyticsSummary();
  console.log(
    `members: ${summary.memberCount}, minted: ${summary.pointsMinted}`,
  );
}

Spend points (member-side)

Members spend points by redeeming an offer. Each offer carries a pointsPrice - an integer points cost (or null for offers that are not points-priced). Redeeming debits pointsPrice from the wallet; the redemption records the spend.
FieldWhereMeaning
pointsPriceOfferInteger points to redeem the offer (null if not points-priced).
If the member’s balance is below pointsPrice, the redemption fails with INSUFFICIENT_POINTS (422). Canceling a redemption refunds its pointsPrice back to the wallet as a credit. The mechanics of submitting, tracking, and canceling a redemption live in Redemptions.
Read pointsPrice against the wallet balance before offering a redeem action, so the member never hits an INSUFFICIENT_POINTS error mid-flow.

Read the balance (member-side)

GET /v1/wallet returns the current Wallet. It is a member-mode call - use the member token.
balance
number
required
The current spendable balance.
amountReceived
number
required
Lifetime total credited to this member.
amountSpent
number
required
Lifetime total debited from this member.
currency
object
required
The wallet currency, with name and symbol.
curl https://api.expys.com/v1/wallet \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN"
The same Wallet is also embedded in the eligibility response, so a single eligibility call gives you tier and balance together:
import { initialize } from "@expys/sdk";

const token = process.env.EXPYS_MEMBER_TOKEN;
if (!token) {
  throw new Error(
    "Set EXPYS_MEMBER_TOKEN (a member token from your backend's /v1/auth/exchange)",
  );
}

const expys = initialize({
  baseUrl: process.env.EXPYS_BASE_URL,
  environment: "sandbox",
  token,
});

async function main(): Promise<void> {
  // externalUserID names the member when a machine token calls on their behalf.
  const eligibility = await expys.eligibility({
    externalUserID: process.env.EXPYS_EXTERNAL_USER_ID,
  });
  console.log(`tier: ${eligibility.tier}`);
  console.log(`wallet (from eligibility): ${eligibility.wallet.balance}`);

  const wallet = await expys.wallet();
  console.log(
    `wallet: balance=${wallet.balance} received=${wallet.amountReceived} ` +
      `spent=${wallet.amountSpent} ${wallet.currency.symbol} (${wallet.currency.name})`,
  );
}

Read the ledger (member-side)

GET /v1/wallet/transactions returns every wallet change as a list of Transaction records, newest first, with cursor pagination. In the SDKs this is walletTransactions(...).
limit
integer
Page size. Defaults to the server’s default if omitted.
cursor
string
The nextCursor from the previous page. Omit for the first page.
externalUserID
string
Names the member when a machine token calls on their behalf.
curl "https://api.expys.com/v1/wallet/transactions?limit=50" \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN"
The response is a ListTransactionsResponse:
transactions
Transaction[]
required
The page of transactions, newest first.
nextCursor
string | null
required
Pass this back as cursor to fetch the next page. null marks the end of the list.

The Transaction schema

FieldTypeMeaning
idstringThe transaction id.
typestringThe transaction kind.
amountnumberSigned: positive for a credit (mint or refund), negative for a debit (spend).
reasonstring | nullFree text - the reason from a mint, or a system label. May be null.
redemptionIDstring | nullSet on a spend or refund to link the transaction to its redemption; null for a plain mint.
createdAtstringISO-8601 timestamp.
amount is signed, so summing the page reconciles against the wallet: credits push balance and amountReceived up, debits push balance down and amountSpent up. Follow redemptionID to tie a debit (and any later refund) back to the redemption that caused it.

Paginate the ledger

Keep passing nextCursor back as cursor until the server returns null:
# First page returns a nextCursor; pass it back to get the next page.
curl "https://api.expys.com/v1/wallet/transactions?limit=50&cursor=CURSOR_FROM_PREVIOUS_PAGE" \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN"
The cursor loop is identical to every other list endpoint in the API, such as offers and the concierge message history.

The loop, end to end

1

Mint

Your backend credits points with the Org-API-Key (POST /v1/wallet/credit). The balance goes up; a credit transaction is written.
2

Spend

The member redeems an offer in the app. pointsPrice is debited; a negative transaction with the redemptionID is written.
3

Refund

If that redemption is canceled, pointsPrice is credited back; a positive transaction carrying the same redemptionID is written.
4

Audit

GET /v1/wallet shows the running balance and lifetime totals; GET /v1/wallet/transactions replays the whole history.

Wallet

The wallet object, balances, and currency in depth.

Redemptions

How spending and refunds work through the redemption lifecycle.

Members

Identifying members by externalUserID and their tier.

Server mode

Backend calls with the Org-API-Key, including minting points.