Skip to content

Concepts

Points and cents, pending vs spendable, tiers, expiry, holds — the vocabulary the API uses.

Updated 2026-09-28

Every field name in the API comes from a small set of ideas. Read this once and the reference will make sense.

Money and points are integers

Every programme runs in one currency — currency (an ISO 4217 code such as INR, USD or JPY) on GET /v1/programs, normally your store's own. Every amount is an integer in hundredths of one unit of that currency, the same convention as Shopify's Liquid prices: gross_cents: 249900 is ₹2,499.00 in an INR programme and $2,499.00 in a USD one, and ¥1,250 is 125000. Points are the same — a programme where 1 point is worth one unit of its currency stores 1 point as 100. The programme's currency_name ("VibeCoins", "Stars") is what members see; locale says how the programme writes numbers. The API never rounds, never uses floats and never converts between currencies.

An order event may say which currency its amounts are in. If it does, it must be the programme's currency — anything else is rejected with 422 validation at path currency, because earning on it would be wrong. Convert first, or leave currency out.

A member

One person in one programme, identified by email (the anchor — it never changes through the API), optionally a phone and your own external_ref. Anywhere the API takes a member id you may send our M-… id, ref:<your id> or phone:<number>. A member has a state: prospect (seen on an order, has not agreed to terms), registered, active, paused, suspended, deleted, anonymised.

The ledger and the four balances

Points live in an append-only ledger: earns, adjustments, redemptions, reversals, expiries. Nothing is ever edited; corrections are new entries. From it we keep four numbers per member:

BalanceMeaning
pending_centsEarned, not yet usable — waiting for a return window or a manual approval.
available_centsUsable now.
reserved_centsHeld by an open checkout reservation.
spendable_centsavailable − reserved: what a redemption may take right now. Use this one.

lifetime_earned_cents and lifetime_redeemed_cents are running totals. Points later taken back by a refund still count in lifetime_earned_cents; a grant that was undone (a reversed import, bulk grant or manual credit) never counts. When an order is edited or corrected, only the corrected earn counts, not the earn it replaced.

Earning

An order.paid (or order.placed / order.fulfilled, depending on the programme's timing) runs the programme's rules — base rate, tier rate, category boosts, campaigns, caps and budgets — and writes earn entries. Whether they land pending or available is the programme's release timing (immediately, on payment, on fulfilment, after the return window). order.refunded and order.cancelled write the reversal. A correction (an order event with supersedes_event_id) reverses the earns it supersedes and earns again on the corrected value, which is the order's value before refunds — refunds already received still stand.

Tiers

Levels a member holds by meeting a threshold (spend or orders) over a window (rolling 365 days, calendar year, lifetime). A tier can change the earn rate and unlock benefits your storefront may read. Tier is evaluated by the programme, not set through the API.

Redeeming: reservations and vouchers

Two ways to spend, both take the row lock so a balance can never go negative by a race:

  • Reservation (custom checkout): quote → reserve (holds points, 15 minutes unless the programme sets another time — the quote's reservation_ttl_minutes says which) → confirm with an order_ref, or release. Points are only debited on confirm.
  • Voucher (till, phone order): POST /v1/members/{id}/redemptions debits immediately and returns a single-use code; confirm it when it is used, void it to return the points; unconfirmed vouchers return their points on expiry.

Expiry and holds

Points can expire under the programme's policy (fixed days, end of period, inactivity). expiring_30d_cents on GET /v1/public/member (the member session token endpoint) tells you what is about to. A hold is a reservation's grip on points; under review is an earn waiting for a person to approve it.

Events are asynchronous

POST /v1/events answers 202 and processes in the background, usually within a second. Every event has a status you can read (queued → running → completed / failed) and an error that says why it failed or why it was ignored (an older aggregate_version, a customer nobody could match). Each event needs its own id for that: without one, an event is keyed by type and order_ref, so a later version of the same order would be refused with 409 rather than recorded. Webhooks tell you when balances change.

Conventions

  • Timestamps are ISO 8601 in UTC: 2026-09-22T10:31:00.000Z.
  • Member ids are the stable public form M-…; internal identifiers never appear.
  • Paged lists return { data, pagination: { cursor } }. Pass ?cursor= for the next page; null means the end. Cursors are stable across inserts, so a walk never skips or repeats a row.
  • Errors are RFC 9457 application/problem+json — see Errors.
  • Responses only gain fields inside /v1 — see Versioning.
Concepts — StickyTier API