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_VIEWpermission on the target site for reading the catalogue, andSETTINGS_MANAGEfor changing it. See authentication. - At least one
VoucherTypeconfigured 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.submittedandbasket.settledif 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:
- A
VoucherTypeis 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. - A
Voucheris 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 correspondingVoucherCodeautomatically. - A
VoucherCodeis 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
Voucherrecord asvalid_offerings; you can edit it post-sale withcreateVoucherValidOfferinganddeleteVoucherValidOffering.
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
- Build a basket and check out — the flow that backs both selling and redeeming vouchers.
- Customer profile, marketing consent, and payment methods — the customer record that vouchers are sold to and redeemed by.
- Manage memberships — the recurring alternative to gift voucher sales for ongoing engagement.
- Endpoint references:
listVoucherTypes,getVoucherType,listVoucherCodes,actionCheckVoucherCode,createBasketVoucher,actionRevokeVoucherCode.
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_untilon 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.settled—basket.submittedis 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.