Guide

Sell and redeem gift vouchers

Sell a gift voucher through the basket flow, validate the code at the point of redemption, redeem it against a future basket, and manage the catalogue around it. This guide covers both sides of the voucher lifecycle — the storefront that sells vouchers and the storefront (or operator workflow) that redeems them.

Before you start

  • A personal access token with the SETTINGS_VIEW permission on the target site for reading the catalogue, and SETTINGS_MANAGE for changing it. See authentication.
  • At least one VoucherType configured on the site. Voucher types define what's on sale — the amount (cash or discount-to-zero), the validity window, the offerings the resulting code can be redeemed against, and the customisable-amount range. The Trybe back office is the source of truth for type configuration.
  • A working understanding of the basket and checkout flow — selling a voucher is a basket submission with a voucher-type line item. Read that guide first if you haven't already.
  • A webhook configuration listening for basket.submitted and basket.settled if you want to drive your own "voucher purchased" workflows. See the webhooks guide.

How vouchers fit together

Three objects make the voucher domain hang together:

  1. A VoucherType is the SKU. It describes a class of voucher you sell — "£50 gift card", "Spa day for two", "Christmas package". One voucher type can have many sold codes.
  2. A Voucher is the underlying purchasable record produced by submitting a basket containing a voucher-type item, or imported from an external system. Trybe-native baskets generate the corresponding VoucherCode automatically.
  3. A VoucherCode is the customer-facing redemption code. Each voucher carries a remaining balance, an expiry window, and a list of eligible offerings. Redeeming a code against a future basket draws down its balance.

Two of those three objects matter for most integrations — the type catalogue (selling) and the code lifecycle (redeeming). The middle Voucher record is usually transparent.

1. List the voucher catalogue

Surface the voucher types on sale at the site so the customer can pick one. The list endpoint takes site_id and returns a paginated set:

curl "https://api.playground.try.be/shop/voucher-types?site_id=00000000-0000-0000-0000-111111111111" \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Accept: application/json"
const res = await fetch(
  'https://api.playground.try.be/shop/voucher-types?' +
    new URLSearchParams({
      site_id: '00000000-0000-0000-0000-111111111111',
    }),
  {
    headers: { Authorization: `Bearer ${process.env.TRYBE_API_KEY}` },
  },
)
const { data: types } = await res.json()

The list filters by archived, amount_type (cash vs discount_to_zero), customisable_amount, and a free-text query. Use these to power the catalogue UI — for example "show only customisable-amount gift cards" or "show only fixed-amount package vouchers".

For a single type's detail page, call getVoucherType with the voucherTypeId:

curl "https://api.playground.try.be/shop/voucher-types/$VOUCHER_TYPE_ID" \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Accept: application/json"

See listVoucherTypes and getVoucherType.

Availability and eligible offerings

Two related resources hang off a voucher type and matter when you're explaining "where can I use this?" on the storefront:

  • Availability rules define the windows in which the resulting code can be redeemed — weekdays only, evenings only, off-peak months only. Fetch them with listVoucherTypeAvailabilityRules.
  • Valid offerings define which appointment types, packages, sessions, or other offerings the code can be redeemed against. For a sold voucher this lives on the Voucher record as valid_offerings; you can edit it post-sale with createVoucherValidOffering and deleteVoucherValidOffering.

Surface both on the catalogue page so the customer knows up-front when and where the voucher can be used. The constraints are enforced server-side at redemption time, so an under-informed customer ends up with a refund request — not a redeemed voucher.

2. Sell a voucher

Selling a voucher is a basket submission. The basket carries one item per voucher being sold, with offering_type: voucher and the offering_id set to the voucher-type ID. The flow is otherwise identical to the basket and checkout flow — create a basket, add the item, set the customer, take payment, and submit.

curl "https://api.playground.try.be/shop/basket/$BASKET_ID/items" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "offering_id": "5f9999999999999999999999",
    "offering_type": "voucher",
    "price": 5000
  }'
$response = Http::asJson()
    ->withToken(config('services.trybe.token'))
    ->post("https://api.playground.try.be/shop/basket/{$basketId}/items", [
        'offering_id'   => '5f9999999999999999999999',
        'offering_type' => 'voucher',
        'price'         => 5000,
    ]);

For voucher types with a customisable amount, pass price in the smallest currency unit — the API enforces the type's configured min_price and max_price. For fixed-amount types, omit price and the server applies the configured amount.

After the basket is submitted (see step 7 of the basket guide), Trybe generates a Voucher and an associated VoucherCode. The code is what the customer will use at redemption time; it's printed on the confirmation email and, depending on the configured VoucherDeliveryOption, on a downloadable PDF.

Watch basket.submitted for the sale event and basket.settled once payment clears. The submitted-order payload includes the issued voucher code(s) so you can drive your own delivery workflow on top of it.

Multiple vouchers in one basket

You can sell several vouchers in one transaction by adding multiple voucher-type items to the same basket. Each item produces its own VoucherCode on submit. This is the right pattern for "buy a stack of gift cards" flows; it's the wrong pattern for "split this £100 voucher across two recipients" — for that, sell two £50 vouchers.

3. List and look up issued codes

For operator-facing tooling — a CS dashboard, a fulfilment job, a "resend my voucher" self-service flow — list and look up the codes produced by submission.

Filtered list

listVoucherCodes returns a paginated list with rich filtering:

curl "https://api.playground.try.be/shop/voucher-codes?site_id=00000000-0000-0000-0000-111111111111&status=valid" \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Accept: application/json"

The filters cover code (substring search), status, amount, balance, created_at, voucher_type_id, expiry window, and fulfillment_status (e.g. needs_dispatching for a physical fulfilment workflow). The result is ordered by created_at descending.

See listVoucherCodes.

Single code

For the operator view of one code, fetch by ID:

curl "https://api.playground.try.be/shop/voucher-codes/$VOUCHER_CODE_ID" \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Accept: application/json"

See getVoucherCode.

Customer-facing lookup

For a public "is my voucher valid?" page on the storefront — where the caller doesn't have a back-office token — use the by-code endpoint. It accepts the customer-typed code and a site_id, validates the code, and returns its current redeemable balance and the offerings it can be applied to:

curl "https://api.playground.try.be/shop/vouchers/codes/GIFT-9F2K-AT3X?site_id=00000000-0000-0000-0000-111111111111" \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Accept: application/json"
const code = 'GIFT-9F2K-AT3X'
const params = new URLSearchParams({
  site_id: '00000000-0000-0000-0000-111111111111',
})

const res = await fetch(
  `https://api.playground.try.be/shop/vouchers/codes/${encodeURIComponent(code)}?${params}`,
  {
    headers: { Authorization: `Bearer ${process.env.TRYBE_API_KEY}` },
  },
)

if (res.status === 400) {
  // Recognised code, but not redeemable: expired, fully spent, etc.
  const { message } = await res.json()
}

if (res.status === 404) {
  // Code does not exist at all.
}

if (res.ok) {
  const { data } = await res.json()
  // data.balance, data.valid_until, data.valid_offerings
}

The endpoint follows a two-tier search: it checks Trybe-native voucher codes first, then if the site has an active voucher integration (e.g. GiftPro) it falls back to the external processor. External vouchers are returned in the same response shape with payment_processor: external_voucher.

A 400 Bad Request here means the code is real but cannot be redeemed — typically expired or fully spent. The response carries a human-readable message; surface it verbatim.

See actionCheckVoucherCode. The equivalent getVoucherLookup endpoint on /shop/voucher-lookup/{voucherCode} returns the same VoucherCodeLookup shape and is what powers the customer-facing "check my voucher" page on the booking engine.

4. Redeem a voucher against a basket

Redemption happens through the basket flow. The full mechanics live in the basket and checkout guide; the voucher-specific bit is one call:

curl "https://api.playground.try.be/shop/basket/$BASKET_ID/vouchers" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "GIFT-9F2K-AT3X" }'
$response = Http::asJson()
    ->withToken(config('services.trybe.token'))
    ->post("https://api.playground.try.be/shop/basket/{$basketId}/vouchers", [
        'code' => 'GIFT-9F2K-AT3X',
    ]);

This attaches the voucher to the basket as a payment method. If the voucher's remaining balance covers the full basket total, the basket can go straight to submit; if it covers only part of the total, the customer pays the rest via card, customer credit, or another voucher.

A 400 Bad Request here surfaces the reason the voucher can't be redeemed: wrong site, expired, fully spent, ineligible offering, or an error from a third-party voucher processor. Show the message; let the customer pick a different code or remove the voucher.

See createBasketVoucher and deleteBasketVoucher.

Partial redemption

If the basket total exceeds the voucher's remaining balance, the attached voucher will only cover part of the total. The basket's outstanding_amount reflects what the customer still owes. Take the remainder via the standard payment flow — typically a card payment through Stripe Checkout, but it could be another voucher, a customer credit, or any combination supported by the site's configured processors.

The voucher's balance is drawn down only when the basket settles, not when the voucher is attached. If the basket is abandoned, the attachment is released and the balance is restored. This is the right model — it means a customer can attach a voucher, change their mind, and not lose access to the unused balance.

Multiple vouchers per basket

You can attach more than one voucher to a single basket; each is applied in attach order against the outstanding total. This is the right pattern for combining low-balance vouchers (a £20 birthday voucher plus a £30 Christmas voucher to cover a £50 booking) without forcing the customer to pick a single one.

5. Bulk-import vouchers

When migrating from a legacy voucher system, you don't want to feed every legacy code through the basket flow one by one. The import endpoint reconciles the catalogue from the site's configured voucher processor in one asynchronous job:

curl "https://api.playground.try.be/shop/vouchers/import?site_id=00000000-0000-0000-0000-111111111111" \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Accept: application/json"

The call returns a QueuedJob immediately — poll its status to track progress. Newly synced vouchers don't appear in listVouchers until the job completes. The import is idempotent: re-running it updates existing records and creates only those that are still missing.

This endpoint is the one-shot tool for "the site has just been configured with GiftPro and there are 12,000 outstanding vouchers in the system — please mirror them". For everyday selling, the basket flow remains the right path.

See queueImportVouchers.

6. Revoke a voucher code

Customer-service teams need to cancel vouchers occasionally — after a chargeback on the purchasing order, in response to a fraud report, or when a customer reports a code as stolen. Revocation is one call:

curl "https://api.playground.try.be/shop/voucher-codes/$VOUCHER_CODE_ID/revoke" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY"

The code moves to status revoked and is no longer redeemable. The redemption history on the code is preserved — revocation never deletes past activity, only prevents future spend.

Revocation is permanent. There is no actionUnrevokeVoucherCode. If you've revoked a code by mistake and the customer still deserves the spending power, issue a fresh voucher of equivalent value instead. See actionRevokeVoucherCode.

7. Subscribe to outcomes

The voucher lifecycle generates the same basket.submitted / basket.settled events you'd expect from any sale (see the webhooks guide). There is no dedicated voucher.created event today — drive your downstream "voucher purchased" workflow from basket.submitted and filter the order's items by offering_type: voucher.

For redemption-side observability, you'll typically watch the resulting basket events too: the redeeming basket emits basket.submitted with the voucher attached as a payment, and basket.settled when the voucher balance has been drawn down.

Going further

Production checklist

  • Treat voucher codes as bearer tokens. Anyone who has the code can redeem it. Don't log full codes in your application logs, don't print them in plain-text email forwards, and don't store them in unencrypted analytics events. Treat them with the same hygiene you apply to API keys.
  • Send delivery emails over signed URLs. When you build your own "your gift voucher" delivery email, link the recipient to a server page that fetches the code via the API rather than embedding the code directly in the URL. URLs leak through referer headers and email-forwarder previews; tokenised server-side fetches don't.
  • Respect the type's expiry rules. Voucher types carry a configured validity window that's enforced at redemption — but if you're printing PDFs or sending physical fulfilment, show the valid_until on the artefact too. Surprises at redemption time are CS overhead.
  • Validate codes server-side, not just on the storefront. The by-code lookup endpoint is convenient for "is my voucher valid?" pages, but the authoritative check happens when the voucher is attached to a basket. Don't pre-validate on the storefront and then skip attachment — the balance can change between the two calls.
  • Surface the partial-redemption case. When a voucher only covers part of a basket, the customer needs a clear "and another £X via card" CTA. The basket payload tells you the outstanding amount; don't hide it behind a "voucher applied" success state.
  • Hold redemption attachment until the customer commits. Attach the voucher only when the customer has confirmed they want to use it. Speculative attachment-on-page-load means a balance is effectively held against an in-progress basket, which is fine as long as the basket is going to be paid — but for browsing sessions it's a poor experience.
  • Subscribe to settlement, not just submission. A voucher attached to a basket draws down its balance only at settlement. If you're tracking voucher balance in your own ledger, watch basket.settledbasket.submitted is too early.
  • Verify webhook signatures. Same as for every other event source — every webhook delivery carries a signature, and unverified deliveries should be rejected. See the webhooks guide.
  • Don't reuse revoked codes. A revoked code is dead permanently. Don't build operator tooling that lets staff revoke-then-restore; the platform refuses, and the customer-facing implication is that the code might come back. Issue a new code.