Skip to main content
A redemption books an offer for a member. Creating one debits the offer’s pointsPrice from the member’s wallet; canceling one refunds those points. This guide covers creating, fetching, and listing redemptions.
These are member-mode calls: they use the member token, not the Org-API-Key. See Authentication. Whether the redemption lands in sandbox or live is selected by the key the token was minted from - see Environments.

Create a redemption

createRedemption(input, options/idempotencyKey?) calls POST /v1/redemptions and returns 201 with the created Redemption. The body is a CreateRedemptionRequest:
FieldTypeDescription
offerstring, requiredThe id of the offer to redeem (from listOffers).
externalUserIDstringNames the member when a machine token acts on their behalf. Optional otherwise.
Redeeming debits the offer’s pointsPrice from the member’s balance. If the balance is below the price, the call fails with INSUFFICIENT_POINTS rather than going negative.
curl
curl -X POST https://api.expys.com/v1/redemptions \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a1b2c3d4-0000-0000-0000-000000000000" \
  -d '{ "offer": "offer_123" }'

Safe retries with an idempotency key

Because creating a redemption spends points, always send an Idempotency-Key header (the SDK accepts it via idempotencyKey). If a network blip makes you retry, the server returns the original redemption instead of charging twice. Reusing a key with a different body is rejected with IDEMPOTENCY_KEY_REUSED.
Generate a fresh, unique key per logical redemption attempt and reuse that same key across retries of that one attempt. See Retries and idempotency for the full contract and how the SDK handles automatic retries.

Fetch a redemption

getRedemption(id) calls GET /v1/redemptions/{id} and returns a single Redemption:
FieldTypeDescription
idstringThe redemption id.
offerOfferThe redeemed offer (see Offers).
statusstringThe current lifecycle status (see below).
createdAtstringISO-8601 creation time.
startAtstring | nullWhen the experience starts, if scheduled.
endAtstring | nullWhen the experience ends, if scheduled.
curl
curl https://api.expys.com/v1/redemptions/red_123 \
  -H "Authorization: Bearer YOUR_MEMBER_TOKEN"

List redemptions

listRedemptions(...) calls GET /v1/redemptions and returns a ListRedemptionsResponse with redemptions and a nextCursor. It accepts limit, cursor, externalUserID, and a status filter. The snippet below pages through a member’s OPEN redemptions:
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,
});

const externalUserID = process.env.EXPYS_EXTERNAL_USER_ID;

async function main(): Promise<void> {
  // Cursor-paginate the member's open redemptions until nextCursor is null.
  let cursor: string | undefined;
  do {
    const page = await expys.listRedemptions({
      cursor,
      externalUserID,
      limit: 50,
      status: "OPEN",
    });
    for (const redemption of page.redemptions) {
      console.log(`redemption ${redemption.id} [${redemption.status}]`);
    }
    cursor = page.nextCursor ?? undefined;
  } while (cursor);

  // The points ledger: each credit/debit on the member's wallet.
  const ledger = await expys.walletTransactions({ externalUserID, limit: 50 });
  for (const transaction of ledger.transactions) {
    console.log(
      `tx ${transaction.id}: ${transaction.type} ${transaction.amount} ` +
        `(${transaction.reason ?? "no reason"})`,
    );
  }
}
ParameterTypeDescription
limitintegerMaximum redemptions per page.
cursorstringThe nextCursor from the previous page. Omit on the first call.
externalUserIDstringFilter to a specific member (when a machine token acts on their behalf).
statusstringFilter to one lifecycle status (see below).
ListRedemptionsResponse is cursor-paginated: follow nextCursor until it is null. Treat the cursor as an opaque token.

Status lifecycle

A redemption moves through these statuses as the experience is booked and fulfilled:
StatusMeaning
SUBMITTEDThe redemption was just created.
OPENIt is open and being processed.
AWAITING_VENDORWaiting on the vendor.
AWAITING_CUSTOMERWaiting on the customer.
PURCHASEDThe booking is purchased.
COMPLETEDThe experience completed.
CANCELEDThe redemption was canceled; the points are refunded.
Canceling a redemption refunds the debited points back to the member’s wallet. See Points and wallet for how the balance reflects debits and refunds.

Errors

Branch on the error code, never on message. The redemption-specific codes are:
CodeStatusMeaning
INSUFFICIENT_POINTS422The member’s balance is below the offer’s pointsPrice.
REDEMPTION_ALREADY_EXISTS409The member already redeemed this offer.
OFFER_UNAVAILABLE422The offer is expired or otherwise not redeemable.
See Errors for the full taxonomy, the shared error shape, and the requestId you quote to support.

Next steps

Points and wallet

How debits and refunds move the member’s balance.

Retries and idempotency

The idempotency-key contract and automatic retry behavior.

Errors

Stable codes, the error shape, and the request id.