Get loyalty token balance

Returns the current loyalty token balance for the authenticated user.

Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…

Why this endpoint exists

On campaigns with a loyalty mechanism, the customer's balance buys down the price of their basket. This is the read for showing it: "you have €12.50 to spend" on the storefront, in the basket, and on an account page.

The balance is a ledger balance — the sum of every booked award and spend. It is not a separately stored number that can fall out of step with the entries behind it; see ledger entries for the individual movements.

Request

curl https://api.acc.funtrips.io/v2/me/loyalty/ledger/balance \
  -H "x-campaign-id: $CAMPAIGN_ID" \
  -H "Authorization: Bearer $END_USER_TOKEN"

Response

{
  "data": {
    "external_user_id": "customer-4815162342",
    "tokens": 1250,
    "last_updated_at": "2026-09-02T09:41:07Z"
  },
  "meta": { "correlation_id": "…" }
}
FieldMeaning
external_user_idYour identifier for the customer
tokensCurrent balance — a bare integer with no unit
last_updated_atWhen the balance last changed

tokens has no unit — you have to look it up

1250 is €12.50 when the campaign's token_unit is cents, and 1250 points when it is points.

The balance carries no unit of its own. Read token_unit from GET /campaign/config and format accordingly.

This is the single most consequential formatting decision in the loyalty surface. Getting it wrong renders a €12.50 balance as "1250 points" — or, worse, renders 1250 points as "€12.50" and promises the customer a hundredfold discount they will not receive.

Fetch the config once at boot, keep the unit alongside your formatter, and never hardcode it: the same integration can serve both kinds of campaign.

function formatBalance(tokens, tokenUnit, currency) {
  if (tokenUnit === 'cents') {
    return new Intl.NumberFormat('nl-NL', { style: 'currency', currency })
      .format(tokens / 100);
  }
  return `${tokens.toLocaleString('nl-NL')} points`;
}

You do not spend the balance yourself

There is no "apply loyalty" call. When a checkout is created with an authenticated customer, the platform resolves their entitlement and applies it server-side; the discount comes back on the checkout lines as a LOYALTY entry in discounts, with the wallet as its source.

So the sequence is: show the balance from this endpoint, create the checkout with the customer's token, and render the discount the response reports. Never compute the discount client-side — the allocator decides how a balance spreads across lines, and it will not match your arithmetic.

A balance lags a fresh write

Entries created through POST /admin/loyalty/ledgers/{id}/entries
are booked asynchronously. A read immediately after that endpoint returns 202 will usually
still show the old balance.

If your flow needs to show the post-award figure right away, compute it locally from the award, or
wait for the loyalty.LedgerBalanceChanged event — do not poll this endpoint in a tight loop
expecting it to change.

Freshness

On campaigns where a partner platform owns the authoritative counter, this endpoint returns the local ledger balance, which can lag the partner's. Call POST /me/loyalty/ledger/sync to reconcile before showing a balance the customer will act on.

For campaigns where the platform owns the balance, this endpoint is authoritative and no sync is needed.

Related

Errors

StatustypeMeaning
401Token missing, expired or invalid
404wallet-balance-not-foundNo balance exists for this customer in this campaign
500Unexpected fault. Retryable

404 means "no wallet yet", not "an error"

A customer who has never earned or spent anything has no balance record. That is the normal state for a new customer on a loyalty campaign.

Render it as a zero balance, not as a failure. Falling over on a 404 here breaks the storefront for exactly the customers you most want to onboard.

const res = await fetch(`${BASE}/me/loyalty/ledger/balance`, { … });
const tokens = res.status === 404 ? 0 : (await res.json()).data.tokens;
Headers
uuid
required

Campaign scope for this request. Filters content to the specified campaign.

string
enum
Defaults to application/json

Generated from available response content types

Allowed:
Responses

Language
Credentials
Bearer
JWT
URL
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
application/json
application/problem+json