Skip to main content
Offers are the catalog of experiences a member can redeem. listOffers returns the offers available to the calling member, each with its points price and optional expiry, paged with a cursor.
Reading the catalog works with either credential. GET /v1/offers and GET /v1/offers/{id} accept your Org-API-Key directly or a member token, so your backend can browse the catalog without minting one. Setting a price is server-side only - PUT/DELETE /v1/offers/{id}/price require the Org-API-Key with config:write and reject a member token.See Authentication. Whether you read the sandbox demo catalog or your live catalog is selected by the key - see Environments.

List offers

listOffers(limit?, cursor?) calls GET /v1/offers and returns an OfferList. The snippet below pages through the entire catalog by following nextCursor until it comes back null:
curl

The Offer schema

Each entry in data is an Offer:
valueUSD: null means “price on request”, not free. The catalog is curated by hand and a few experiences are quoted at booking. Render those as a quote-on-request rather than as a zero.
Every field added above is nullable except availability and inclusions. No experience carries all of them - build for null on each. description is unchanged and still contains the full editorial copy, including prose forms of what is now structured. Render the structured fields and use description for the narrative paragraph.

Prices in your own currency

display carries the price in your members’ currency, so you never have to do the arithmetic or explain a points figure:
Read display.amount and render it with display.currency.symbol. Read unitsPerUSD from the response rather than hardcoding it - you can change it yourself at any time, and a hardcoded copy would silently disagree with the API the moment you did. See Currency configuration. display.source tells you where the number came from:
amount is a number, not an integer. If a credit is worth $1,000 and an experience is $1,650, the answer is 1.65 - and rounding that to a whole credit would move real money between what you charge your member and what you pay us. Nothing is rounded: a derived price is exact, and an authored price is returned exactly as you set it, including 39.5.This changed in 0.5.0. If you are on the Kotlin or Swift SDK, amount and the wallet *Display fields moved from Int to Double - upgrade before you take the new server behaviour. TypeScript and raw HTTP need no change.

Setting your own price per experience

A single ratio makes every price a mechanical function of our cost. When you want to price a $75,000 experience differently from a $2,000 one, author the price yourself:
The response is the whole offer, so you see exactly what your members will. To go back to the derived figure:
Both need the config:write scope, and both are server-to-server only.
An authored price changes what you charge, not what you pay. Settlement always debits pointsPrice, whatever display.amount says. The spread between the two is yours. Authored prices require pool settlement - on member-wallet settlement the request is refused with OFFER_PRICING_UNAVAILABLE, because a member’s balance is denominated in points you have already paid us for.

Availability and sold-out experiences

Every offer carries an availability object: isAvailable is not simply quantityRemaining > 0: an experience with no points price also cannot be redeemed, so it reports false while quantityRemaining stays truthful. Branch on isAvailable, not on the count. The default list still includes sold-out experiences. That is deliberate - a card that greys out reads better than one that vanishes between two nightly cache loads. To hide them, opt in:
curl

Read a single offer

GET /v1/offers/{id} returns exactly the shape the list returns for that offer - one serializer, so the two cannot drift. Use it for deep links.
curl
An id that does not exist, or belongs to another organization, returns 404 with the standard error envelope. The two responses are identical: the platform does not confirm whether another tenant’s offer exists.
pointsPrice is your price, not a catalogue-wide one. Two accounts can see a different pointsPrice for the same id: we price each experience for each account from its cost and your commercial terms, rounded to your agreed points increment. The value returned is always exactly what a redemption of that offer will debit you, and display.amount is derived from it. The field, its type and its meaning are unchanged; if your commercial terms change, we tell you before any price moves.
pointsPrice of null is distinct from 0: it means the offer carries no points price at all. An offer past its expiresAt is no longer redeemable - attempting to redeem it returns OFFER_UNAVAILABLE. See Redemptions.

Pagination

OfferList is cursor-paginated:
Pagination is cursor-based, not page-numbered. Pass the previous response’s nextCursor as the next request’s cursor, and stop when nextCursor is null. Do not assume a fixed page size or construct cursors yourself - treat the cursor as an opaque token.

Next steps

Redemptions

Redeem an offer, spend points, and follow the redemption lifecycle.

Points and wallet

How points are minted, spent, and tracked against pointsPrice.

API reference

The full GET /v1/offers schema and an interactive playground.