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 or an existing order, 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_to_date, 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.

Four fields in the response decide how the code can be redeemed:

  • amount_type is cash for a voucher that holds a monetary balance, or discount_to_zero for one that brings an eligible item down to zero, such as a "Spa day for two" voucher. External vouchers are always cash.
  • partially_redeemable says whether a cash voucher stays open after a redemption that doesn't use all of it. See partial redemption. A discount_to_zero voucher reports true only if it's a multi-use code.
  • payment_processor is voucher for a code issued by Trybe, or external_voucher for one issued by a voucher integration.
  • uses_remaining is how many more times a discount_to_zero code can be redeemed. A single-use code reports 1 until it's redeemed. It's null for cash vouchers and left out for external vouchers.

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.

If the basket total is smaller than the voucher's balance, the voucher only pays the outstanding amount. What happens to the rest depends on partially_redeemable:

  • If it's true, the leftover balance stays on the voucher, which moves to status partially_redeemed and can be spent later.
  • If it's false, the voucher is used up. It moves to status redeemed and the remainder is written off. The code's balance can still show the remainder, but it can't be spent.

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. Redeem a voucher against an existing order

Use this flow when the order already exists, for example when a point-of-sale system settles a bill. A basket takes every voucher through one call, but an order uses a different call depending on the voucher. Look the code up first (see step 3), then choose from the payment_processor and amount_type fields:

payment_processor amount_type Call
voucher cash createOrderPayment with processor: voucher
voucher discount_to_zero createOrderVoucher
external_voucher cash createOrderPayment with processor: external_voucher

Both calls need the RESERVATIONS_MANAGE permission on the order's site.

Cash and external vouchers

Add the voucher as a payment, with the code in processor_data.code:

curl "https://api.playground.try.be/shop/orders/$ORDER_ID/payments" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2500,
    "processor": "voucher",
    "processor_data": { "code": "GIFT-9F2K-AT3X" }
  }'

amount is required and can't exceed the order's outstanding balance. For a voucher payment, Trybe caps the payment at the voucher's balance and the eligible items on the order, so read amount from the response rather than assuming it matches your request. Take any remainder with another payment.

Sending a discount_to_zero voucher with processor: voucher returns a 400 Bad Request.

Discount-to-zero vouchers

Send the code to the order's vouchers endpoint. There's no amount, because the voucher brings an eligible item down to zero:

curl "https://api.playground.try.be/shop/orders/$ORDER_ID/vouchers" \
  -X POST \
  -H "Authorization: Bearer $TRYBE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "code": "SPADAY-7H3M-QX2P" }'

The voucher applies to the highest-priced eligible item that no other voucher covers. A multi-use code covers one item per remaining use. The updated order is returned, with the new payment in its payments array.

Sending a cash voucher to this endpoint returns a 400 Bad Request.

What happens next

A voucher payment on a submitted order is marked as paid straight away, so the voucher is drawn down immediately rather than at settlement. Leftover balance follows the rules in partial redemption.

See createOrderPayment and createOrderVoucher.

6. 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.

7. 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.

8. 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_to_date 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.
  • Warn before a remainder is lost. If partially_redeemable is false and the bill is smaller than the voucher's balance, the remainder is written off. Tell the customer before you redeem it.
  • 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.settled — basket.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.