Cart Operations & Checkout
Create shopping carts, manage customer info and discounts, and process checkout with Stripe payments using the ResytechApi client.
This page covers the transactional side of the booking flow: building a cart, applying discounts, and completing checkout. It also covers gift card purchasing and Activity Voucher purchasing and redemption.
All examples assume you have initialized the API client and selected an activity, date, and time slot.
Shopping Cart
createOrUpdateCart
Create a new cart or update an existing one. The server validates availability and returns pricing details.
const result = await api.cart.createOrUpdateCart({
cart: {
activity: 'activity-uuid',
duration: 'duration-uuid',
date: '2025-07-15',
timeSlotStart: '10:00',
timeSlotEnd: '11:00',
equipment: [
{
equipmentUuid: 'equipment-uuid',
quantity: 1,
seats: 2,
addons: [
{
addonUuid: 'addon-uuid',
quantity: 1,
equipmentUuid: 'equipment-uuid'
}
]
}
]
}
});
if (result.success) {
console.log('Cart ID:', result.cartId);
console.log('Total:', result.cart.total);
console.log('Stripe account:', result.operatorAccountId);
}To update an existing cart, pass the cartId:
const updated = await api.cart.createOrUpdateCart({
cartId: 'existing-cart-id',
cart: {
activity: 'activity-uuid',
duration: 'duration-uuid',
date: '2025-07-15',
timeSlotStart: '14:00',
timeSlotEnd: '15:00',
equipment: [
{ equipmentUuid: 'equipment-uuid', quantity: 1, seats: 4 }
]
}
});CreateOrUpdateCartRequest
| Field | Type | Required | Description |
|---|---|---|---|
cart | ClientShoppingCart | Yes | Cart contents (see below) |
cartId | string | No | Existing cart ID to update; omit to create new |
CreateOrUpdateShoppingCartResponse
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the operation succeeded |
cartId | string | Cart identifier for subsequent operations |
cart | ServerShoppingCartOverview | Server-calculated pricing and line items |
errorCode | ShoppingCartErrorCode | Numeric error code (if failed) |
removals | ShoppingCartRemoval[] | Items removed due to validation (e.g., sold out equipment) |
clientSecret | string | Stripe PaymentIntent client secret (for deferred payment setup) |
operatorAccountId | string | Stripe connected account ID |
ClientShoppingCart
The core cart object sent to the server. Used by both createOrUpdateCart and checkout.
| Field | Type | Required | Description |
|---|---|---|---|
activity | string | Yes | Activity UUID |
duration | string | Yes | Duration UUID |
date | string | Yes | Booking date (YYYY-MM-DD) |
timeSlotStart | string | Yes | Start time (HH:mm) |
timeSlotEnd | string | Yes | End time (HH:mm) |
equipment | ClientShoppingCartEquipment[] | No | Equipment selections |
customer | ClientShoppingCartCustomer | No | Customer info (required for checkout) |
downPaymentRequested | boolean | No | Request down payment instead of full payment |
isPrivateTour | boolean | No | Book as a private tour. Honoured only when the activity has allowPrivateTours; forced on when it has privateToursOnly. |
partySize | number | No | Tours: the actual number of people. Distinct from equipment seats, which is the capacity reservation. On a private Per Person tour send the full vehicle capacity in seats and the real party here; on every other tour the server reconciles seats to the party. When demographics are also sent the two must agree. |
durationMins | number | No | Dynamic duration in minutes |
tripProtectionSelected | boolean | No | Opt into trip protection |
ClientShoppingCartEquipment
| Field | Type | Required | Description |
|---|---|---|---|
equipmentUuid | string | Yes | Equipment UUID |
quantity | number | Yes | Number of units |
seats | number | Yes | Number of seats (guests) |
addons | ClientShoppingCartEquipmentAddon[] | No | Add-ons for this equipment |
ClientShoppingCartEquipmentAddon
| Field | Type | Required | Description |
|---|---|---|---|
addonUuid | string | Yes | Add-on UUID |
quantity | number | Yes | Quantity |
equipmentUuid | string | Yes | Equipment UUID this add-on is for |
Add-on quantity always means selected units. Eligibility responses include priceType (0 flat, 1 per guest), extendsDurationMinutes, and extendsDurationPerUnit. The server resolves terms from the catalog and calculates the guest multiplier. Do not multiply the quantity sent to checkout by guests.
Send demographics, partySize, isPrivateTour, and current add-on quantities in the equipment rows to add-on eligibility. Refresh eligibility when these or the departure change. The cart response's endDate, endTime, durationMinutes, and addonDurationMinutes describe the complete extended booking. Dates and times are location-local; do not append a UTC suffix. Keep the original base slot and duration in checkout requests.
ClientShoppingCartCustomer
| Field | Type | Required | Description |
|---|---|---|---|
fullName | string | No | Customer full name |
email | string | No | Email address (required for checkout) |
countryCode | string | No | Phone country code (e.g., "1" for US) |
country | string | No | Country name or code |
phone | string | No | Phone number |
ServerShoppingCartOverview
The server-side cart returned in responses. Contains calculated pricing, fees, and applied discounts.
| Field | Type | Description |
|---|---|---|
lineItems | CartLineItem[] | Itemized charges |
fees | Fee[] | Taxes and fees |
subtotal | number | Subtotal before fees and discounts |
total | number | Total after all calculations |
feesTotal | number | Sum of all fees |
discountTotal | number | Sum of all discounts |
paid | number | Amount already paid |
balance | number | Remaining balance |
couponCode | string | Applied coupon code |
giftCardCode | string | Applied gift card code |
giftCardAmountApplied | number | Gift card amount used |
giftCardBalance | number | Remaining gift card balance |
voucherAmountApplied | number | Prepaid value covered by applied Activity Vouchers. Render as a negative row; total does not subtract it |
vouchers | VoucherSummary[] | Applied Activity Vouchers — name, masked suffix, coverage lines. Never the code |
amountDue | number | total net of gift-card and voucher tender. Server-computed bottom line |
dueNow | number | Amount due at checkout |
dueLater | number | Amount due later (for down payments) |
tripProtectionSelected | boolean | Whether trip protection is selected |
tripProtectionPrice | number | Trip protection price |
equipment | ServerShoppingCartEquipment[] | Confirmed equipment selections |
CartLineItem
| Field | Type | Description |
|---|---|---|
name | string | Line item description |
price | number | Amount |
type | string | Item type identifier |
Fee
| Field | Type | Description |
|---|---|---|
uuid | string | Fee identifier |
name | string | Fee display name |
amount | number | Fee amount |
type | TaxesAndFeesType | 0 = tax, 1 = fee |
updateCustomer
Update customer information on an active cart.
const result = await api.cart.updateCustomer({
customer: {
fullName: 'Jane Smith',
email: 'jane@example.com',
phone: '5551234567',
countryCode: '1',
country: 'US'
},
cartId: 'cart-id' // optional if using session-based cart
});UpdateShoppingCartCustomerRequest
| Field | Type | Required | Description |
|---|---|---|---|
customer | ClientShoppingCartCustomer | Yes | Customer details |
cartId | string | No | Cart ID |
Coupons
applyCoupon
const result = await api.cart.applyCoupon({
couponCode: 'SUMMER20',
cartId: 'cart-id'
});removeCoupon
const result = await api.cart.removeCoupon({
cartId: 'cart-id'
});ApplyCouponRequest
| Field | Type | Required | Description |
|---|---|---|---|
couponCode | string | No | Coupon code to apply |
cartId | string | No | Cart ID |
Gift Cards (on Cart)
applyGiftCard
const result = await api.cart.applyGiftCard({
giftCardCode: 'GC-ABC123',
cartId: 'cart-id'
});
if (result.success) {
console.log('Balance available:', result.availableBalance);
}removeGiftCard
const result = await api.cart.removeGiftCard({
cartId: 'cart-id'
});ApplyGiftCardRequest
| Field | Type | Required | Description |
|---|---|---|---|
giftCardCode | string | No | Gift card code |
cartId | string | No | Cart ID |
ApplyGiftCardResponse
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the operation succeeded |
errorCode | GiftCardErrorCode | Error code (if failed) |
availableBalance | number | Remaining gift card balance |
Activity Vouchers (on Cart)
An applied Activity Voucher prepays the part of the cart it covers (a specific activity, duration, eligible equipment and any included add-ons, within the voucher's season). Anything it does not cover — a third rental when you hold two vouchers, an add-on that is not included — stays payable. Vouchers do not combine with coupons or gift cards.
updateVouchers
This is the only call that changes a cart's vouchers. createOrUpdateCart and checkout reject voucher fields. Send the full current cart so coverage is priced against the live selection; the response is a normal cart response.
const result = await api.cart.updateVouchers({
cart: currentCart, // the same ClientShoppingCart you sync
cartId: 'cart-id',
voucherCodes: ['AV0123456789ABCDEF0123456789ABCDEF'],
retainedVoucherIds: [] // ids from a previous response's cart.vouchers[] to keep
});
if (result.success) {
result.cart.vouchers.forEach(v =>
console.log(`${v.name} ····${v.suffix}: ${v.coverage.join(', ')}`)
);
console.log('Prepaid:', result.cart.voucherAmountApplied);
console.log('Still due:', result.cart.dueNow);
} else if (result.errorCode === 29) {
// VoucherInvalid — unknown, used, expired, wrong activity/duration/equipment,
// outside its season or blackout window. result.message says which. Nothing
// was applied or silently dropped.
}Codes are resolved once and never stored on the cart, so to keep vouchers applied earlier you pass their ids back in retainedVoucherIds on every later call. Leave an id out to remove that voucher; send voucherCodes: [] with no retained ids to remove all.
// Add a second voucher, keeping the first
await api.cart.updateVouchers({
cart, cartId,
voucherCodes: ['AV...SECOND'],
retainedVoucherIds: [first.cart.vouchers[0].id]
});
// Remove every voucher
await api.cart.updateVouchers({ cart, cartId, voucherCodes: [] });If the customer later changes equipment, duration or date, re-sync with createOrUpdateCart as usual — the server re-validates the retained vouchers against the new selection and fails the sync with VoucherInvalid if one no longer applies, rather than dropping it.
UpdateCartVouchersRequest
| Field | Type | Required | Description |
|---|---|---|---|
cart | ClientShoppingCart | Yes | Current cart |
cartId | string | No | Cart ID |
voucherCodes | string[] | Yes | New codes to apply (may be empty) |
retainedVoucherIds | string[] | No | Ids of already-applied vouchers to keep |
VoucherSummary
| Field | Type | Description |
|---|---|---|
id | string | Voucher id — pass back in retainedVoucherIds |
suffix | string | Last characters of the code, for "ending 1A2B" copy |
name | string | Offer name |
quantity | number | Units this voucher covers |
prepaidAmount | number | Value it contributes to voucherAmountApplied |
coverage | string[] | Human-readable lines describing what it covers on this cart |
redemptionId | string | Set once the voucher is reserved against a booking |
redemptionStatus | number | 0 Held, 1 Committed, 2 Released, 3 Restored, 4 Forfeited. Null on a cart preview |
Checking out a cart with vouchers has two extra fields — see CheckoutRequest — and an interrupted attempt must be reconciled before starting over — see Activity Voucher Redemption.
Trip Protection
getTripProtectionPreview
Preview trip protection pricing before the customer opts in.
const preview = await api.cart.getTripProtectionPreview({
cartId: 'cart-id'
});
if (preview.success && preview.available) {
console.log(`${preview.title}: $${preview.price}`);
console.log(preview.description);
}TripProtectionPreviewRequest
| Field | Type | Required | Description |
|---|---|---|---|
cartId | string | No | Cart ID |
TripProtectionPreviewResponse
| Field | Type | Description |
|---|---|---|
available | boolean | Whether trip protection is offered |
price | number | Price for trip protection |
title | string | Display title |
description | string | Display description |
coverageType | number | Coverage type identifier |
coverageAmount | number | Coverage amount |
Checkout
checkout
Process the booking. Requires a complete cart with customer email, and typically a Stripe confirmation token for payment.
const result = await api.checkout.checkout({
cart: {
activity: 'activity-uuid',
duration: 'duration-uuid',
date: '2025-07-15',
timeSlotStart: '10:00',
timeSlotEnd: '11:00',
equipment: [
{ equipmentUuid: 'equip-uuid', quantity: 1, seats: 2 }
],
customer: {
fullName: 'Jane Smith',
email: 'jane@example.com',
phone: '5551234567'
},
tripProtectionSelected: false
},
stripeConfirmationToken: 'tok_xxx', // from Stripe.js
agreements: ['agreement-uuid-1'],
customFields: [
{ uuid: 'field-uuid', answer: 'Some answer' }
],
demographics: [
{ demographicUuid: 'demo-uuid', uuid: 'value-uuid', value: 2 }
],
smsOptIn: true
});
if (result.success) {
console.log('Confirmation:', result.confirmation);
} else if (result.clientSecret) {
// Stripe requires additional action (3D Secure, etc.)
// Use Stripe.js to handle the next action, then retry
console.log('Payment requires additional action');
}CheckoutRequest
| Field | Type | Required | Description |
|---|---|---|---|
cart | ClientShoppingCart | Yes | Complete cart with customer info |
stripeConfirmationToken | string | No | Stripe confirmation token for payment |
handledNextAction | string | No | Stripe PaymentIntent ID after handling 3DS |
customFields | CustomFieldAnswer[] | No | Answers to custom fields |
demographics | DemographicValue[] | No | Guest demographic breakdown |
agreements | string[] | No | UUIDs of accepted agreements |
smsOptIn | boolean | No | Whether customer opts into SMS |
cartId | string | No | Cart ID |
fingerprint | string | No | Optional device fingerprint forwarded to fraud checks |
voucherCheckout | boolean | No | Set true when cart.vouchers is non-empty. The server reserves the vouchers under the booking lock; when dueNow is 0 the booking completes with no card |
expectedVoucherDueNow | number | No | The dueNow you showed on the voucher-covered cart. Rejected if coverage changed since, so a lost voucher never silently charges full price |
With vouchers applied, omit stripeConfirmationToken when dueNow is 0. If the response is lost or the tab closes mid-checkout, do not start a new cart: call voucherRedemption.checkoutStatus first.
CustomFieldAnswer
| Field | Type | Description |
|---|---|---|
uuid | string | Custom field UUID |
answer | string | Customer's answer |
DemographicValue
| Field | Type | Description |
|---|---|---|
demographicUuid | string | Demographic UUID |
uuid | string | Value UUID |
value | number | Count (e.g., 2 adults, 1 child) |
CheckoutResponse
| Field | Type | Description |
|---|---|---|
success | boolean | Whether checkout completed |
confirmation | string | Booking confirmation number |
cart | ServerShoppingCartOverview | Final cart summary |
cartAction | CreateOrUpdateShoppingCartResponse | Cart validation result |
clientSecret | string | Stripe client secret (if further payment action needed) |
paymentIntentId | string | Stripe PaymentIntent ID |
Gift Card Purchasing
These endpoints let customers purchase new gift cards (separate from applying a gift card to a cart).
getSettings
Get the gift card purchase configuration for the location.
const settings = await api.giftCardPurchase.getSettings();
if (settings.success && settings.isAvailable) {
console.log('Flat amounts:', settings.flatAmounts);
console.log('Variable:', settings.allowVariableAmounts,
`$${settings.variableMinAmount}-$${settings.variableMaxAmount}`);
}GetGiftCardPurchaseSettingsResponse
| Field | Type | Description |
|---|---|---|
isAvailable | boolean | Whether gift card purchasing is enabled |
allowFlatAmounts | boolean | Whether preset amounts are offered |
flatAmounts | number[] | Preset dollar amounts |
allowVariableAmounts | boolean | Whether custom amounts are allowed |
variableMinAmount | number | Minimum custom amount |
variableMaxAmount | number | Maximum custom amount |
requireRecipientEmail | boolean | Whether recipient email is required |
allowCustomMessage | boolean | Whether a custom message is allowed |
expirationDays | number | Number of days until the gift card expires |
purchase
Purchase a gift card with Stripe payment.
const result = await api.giftCardPurchase.purchase({
amount: 50,
purchaserName: 'John Doe',
purchaserEmail: 'john@example.com',
recipientName: 'Jane Smith',
recipientEmail: 'jane@example.com',
customMessage: 'Happy birthday!',
stripeConfirmationToken: 'tok_xxx'
});
if (result.success) {
console.log('Gift card code:', result.giftCardCode);
console.log('Amount:', result.giftCardAmount);
console.log('Expires:', result.expiresAt);
}PurchaseGiftCardRequest
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Gift card amount in dollars |
purchaserName | string | No | Buyer's name |
purchaserEmail | string | No | Buyer's email |
purchaserPhone | string | No | Buyer's phone |
purchaserPhoneCountryCode | string | No | Phone country code |
recipientName | string | No | Recipient's name |
recipientEmail | string | No | Recipient's email |
customMessage | string | No | Personal message |
stripeConfirmationToken | string | No | Stripe token for payment |
handledNextAction | string | No | Stripe PaymentIntent ID after 3DS |
PurchaseGiftCardResponse
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the purchase succeeded |
giftCardCode | string | The gift card code |
giftCardAmount | number | Amount loaded |
expiresAt | Date | Expiration date |
recipientEmail | string | Where the gift card was sent |
clientSecret | string | Stripe client secret (if 3DS required) |
paymentIntentId | string | Stripe PaymentIntent ID |
Activity Voucher Purchasing
An Activity Voucher is a prepaid entitlement to a specific experience — an activity, duration, set of eligible equipment and any included add-ons, redeemable within a season — not a dollar balance. Operators publish an offer at a slug; a customer buys one or more vouchers in a single call and receives one bearer code (AV + 32 hex) per voucher, shown once in the response and emailed.
If you want the hosted purchase page instead of building your own, use Activity Voucher Mode on the modal or the voucher-slug attribute on <resytech-booking-embed>.
Voucher responses wrap their payload in data. Failures come back as success: false with a string errorCode — see VoucherErrorCode.
getOffer
Quote an offer for a quantity. The quote is server-authoritative: frozen terms, unit price, fees, taxes and the grand total. Re-quote whenever the customer changes the quantity; the revisionId and pricing.grandTotal you send on purchase must match what you showed.
const quote = await api.voucherPurchase.getOffer('black-friday-kayak', 2);
if (quote.success) {
const { offerId, revisionId, settings, pricing, activity, timeZone } = quote.data;
console.log(settings.name, settings.description);
console.log('Covers:', settings.terms.activityName, settings.terms.durationMinutes, 'min');
console.log('Season:', settings.terms.experienceStartsOn, '→', settings.terms.experienceEndsOn);
console.log('Total for 2:', pricing.grandTotal, pricing.currency);
}Only offers whose settings.terms.entitlementType is 1 (equipment rental) can be bought online today.
VoucherOfferQuote
| Field | Type | Description |
|---|---|---|
offerId | string | Offer id — send on purchase |
revisionId | string | Changes whenever the operator edits the offer — send on purchase |
slug | string | Offer slug |
timeZone | string | IANA time zone of the selling location, for rendering dates in the terms |
settings | VoucherOfferSettings | Name, description, price, sale window, per-order maximum, cancellation policy and the frozen terms |
pricing | VoucherPurchasePricing | quantity, unitPrice, subtotal, fees, taxes, grandTotal, currency, itemized charges[] |
activity | VoucherActivityDisplay | Current name, description and media of the covered activity, for presentation |
VoucherOfferTerms (settings.terms)
| Field | Type | Description |
|---|---|---|
activityUuid, activityName | string | The covered activity |
durationUuid, durationMinutes | string, number | The covered duration |
entitlementType | 1 | 2 | 1 equipment rental, 2 admission |
quantity | number | Units covered per voucher |
equipmentUuids | string[] | Equipment the voucher may be applied to (any one per unit) |
equipmentNames | Record<string,string> | Display names keyed by uuid |
includedAddons | VoucherIncludedAddon[] | Add-ons included per voucher: { equipmentUuid, addonUuid, name, quantity } |
bookingStartsAt, bookingEndsAt | string | Date | When the customer may make the booking. Null = unrestricted |
experienceStartsOn, experienceEndsOn | string | Redeemable experience dates, inclusive, YYYY-MM-DD |
daysOfWeek | number[] | 0 Sunday … 6 Saturday. Empty = any day |
excludedWindows | VoucherDateWindow[] | Blackout ranges: { startsOn, endsOn, startsAt?, endsAt? } |
Timestamps in voucher payloads are typed string | Date; always pass them through new Date(value).
newRequestIdentity
Purchase is idempotent on a browser-minted identity. Mint it before collecting payment and keep it in sessionStorage until the outcome is known; every retry, 3DS resume and outcome probe re-sends the same pair and lands on the same order. Never persist the Stripe token.
const identity = api.voucherPurchase.newRequestIdentity();
// { requestId: '<uuid>', accessToken: '<43-char base64url>' }
sessionStorage.setItem('voucher-attempt', JSON.stringify({ identity, offerId, revisionId, quantity, expectedTotal }));purchase
One call creates the order, binds its payment intent and confirms it with a Stripe confirmation token from a deferred-mode Payment Element (mode: 'payment', amount from the quote). There is no separate "create order" step and no captcha.
let result = await api.voucherPurchase.purchase({
...identity,
offerId,
revisionId,
quantity: 2,
expectedTotal: pricing.grandTotal,
purchaserName: 'Alex Morgan',
purchaserEmail: 'alex@example.com',
recipientName: 'Jamie Lee', // optional — makes it a gift
recipientEmail: 'jamie@example.com',
acceptTerms: true,
stripeConfirmationToken: confirmationToken.id
});Branch on data.action:
const request = { ...identity, offerId, revisionId, quantity: 2, expectedTotal, purchaserName, purchaserEmail, acceptTerms: true };
if (result.success && result.data.action === 'next_action') {
// 3D Secure: complete it, then continue with the intent id and NO token
await stripe.handleNextAction({ clientSecret: result.data.clientSecret });
result = await api.voucherPurchase.purchase({ ...request, handledNextAction: result.data.paymentIntentId });
}
while (result.success && result.data.action === 'processing') {
await new Promise(r => setTimeout(r, 2000));
result = await api.voucherPurchase.purchase({ ...request, handledNextAction: result.data.paymentIntentId });
}
if (result.success && result.data.action === 'success') {
const order = result.data.order;
if (order.fulfillmentStatus === 1) {
order.vouchers.forEach(v => console.log('Code:', v.code, 'paid', v.paidAmount));
} else if (order.fulfillmentStatus === 2) {
// Paid but held for operator review; codes withheld and nothing is emailed
// until the operator resolves the order. Show order.orderId as the reference.
}
sessionStorage.removeItem('voucher-attempt');
} else if (result.success && result.data.action === 'payment_error') {
// Card declined. Keep the SAME identity, collect a new token, call purchase again.
showError(result.data.message);
} else if (!result.success) {
switch (result.errorCode) {
case 'QuoteChanged': // operator edited the offer — drop the attempt and re-quote
case 'SoldOut':
case 'OrderExpired': // 20 minutes passed without payment — start a new identity
sessionStorage.removeItem('voucher-attempt');
break;
case 'PaymentUnavailable':
// Stripe was unreachable. Retry the same request; do not mint a new identity.
break;
}
}Resuming after a reload. With a saved attempt, call purchase with the saved identity and neither payment field. success means it already went through (codes were emailed — do not re-show them). processing means keep polling. errorCode: 'PaymentRequired' means no order exists for that request, so it is safe to start over. Anything else: drop the attempt.
PurchaseVoucherOrderRequest
| Field | Type | Required | Description |
|---|---|---|---|
requestId | string | Yes | From newRequestIdentity() |
accessToken | string | Yes | From newRequestIdentity(). Authorizes access to this order on retries |
offerId | string | Yes | From the quote |
revisionId | string | Yes | From the quote |
quantity | number | Yes | 1 to settings.maxQuantityPerOrder |
expectedTotal | number | Yes | The quote's pricing.grandTotal you showed |
purchaserName | string | Yes | |
purchaserEmail | string | Yes | Receives the voucher email (every code plus the terms) unless a recipient is given |
recipientName | string | No | Gift recipient |
recipientEmail | string | No | Gift recipient. When set, the single voucher email goes here instead of to the purchaser; the purchaser gets no email and must save the codes from the response |
acceptTerms | boolean | Yes | Must be true |
stripeConfirmationToken | string | No | Required to create a new order. Omit to probe or resume |
handledNextAction | string | No | PaymentIntent id after 3DS, or to poll a processing payment |
VoucherPurchaseResult (data)
| Field | Type | Description |
|---|---|---|
action | 'success' | 'next_action' | 'processing' | 'payment_error' | What to do next |
order | VoucherOrder | The order: orderId, paymentStatus, fulfillmentStatus (0 pending, 1 issued, 2 blocked), expiresAt, purchaser/recipient, settings, pricing, vouchers[] ({ id, code, status, paidAmount }), deliveries[] |
clientSecret | string | Set on next_action |
paymentIntentId | string | Set on next_action and processing |
message | string | Customer-facing text on payment_error |
VoucherErrorCode
| Code | Meaning | What to do |
|---|---|---|
QuoteChanged | Offer or price changed since the quote | Drop the attempt, re-quote |
SoldOut | The offer's sales limit is reached | Drop the attempt |
OrderExpired | The unpaid order timed out (20 min) or was released | Mint a new identity |
PaymentRequired | Probe with no token found no order | Safe to start over |
PaymentMismatch | The payment on file does not match this request | Contact the operator |
RequestConflict, AccessTokenConflict | The identity is already bound to a different request | Mint a new identity |
NotFound, Unavailable, OutsideSaleWindow, SaleEnded, InvalidOffer | The offer cannot be sold right now | Show message |
InvalidRequest, InvalidPrice | Validation failure | Show message |
PaymentsUnavailable, PaymentUnavailable | Operator payments not configured / Stripe unreachable | Retry the same request later |
Activity Voucher Redemption
Redemption is an ordinary booking with vouchers applied to the cart. The hosted flow lives at /vouchers/redeem on the booking UI; with the API it is four steps:
voucherRedemption.inspect(code)to check the code and learn which activity it covers.- Build a cart for that activity with
createOrUpdateCartas usual. cart.updateVouchersto apply the code(s).checkoutwithvoucherCheckout: trueandexpectedVoucherDueNow.
normalizeCode
Turns user input into the canonical code shape (upper-case, no spaces or dashes). Returns null when it cannot be a voucher code, so you can validate before requesting.
const code = api.voucherRedemption.normalizeCode(input.value); // 'AV…' or nullinspect
Look up what a code covers without applying it. Returns the offer terms and a masked suffix — never the code.
const result = await api.voucherRedemption.inspect(code, cartId);
if (result.success) {
const { suffix, settings, timeZone } = result.data;
console.log(`${settings.name} (ending ${suffix}) covers ${settings.terms.activityName}`);
// Send the customer to pick a date for settings.terms.activityUuid
} else {
console.log(result.message); // unknown, used, voided, refunded, outside its booking window…
}Rate-limited alongside coupon and gift card lookups.
VoucherInspection (data)
| Field | Type | Description |
|---|---|---|
id | string | Voucher id |
suffix | string | Last characters of the code |
settings | VoucherOfferSettings | The offer, including terms |
timeZone | string | IANA time zone of the location |
checkoutStatus
A voucher checkout reserves the vouchers under the booking lock. If the checkout response is lost, comes back processing, or the tab closes, the cart is in an unknown state and a fresh attempt could double-book or strand the reservation. Read the state first:
const status = await api.voucherRedemption.checkoutStatus(cartId);
switch (status.data?.state) {
case 'confirmed': // booking exists — status.data.confirmation is its code
break;
case 'resume': // re-post the IDENTICAL checkout request, adding
// handledNextAction: status.data.paymentIntentId when present
break;
case 'resolving': // the server is reconciling — wait 2s and call again
break;
case 'resolved': // closed without a booking — the customer may start a new cart
break;
case 'unknown': // nothing saved for this cart
break;
}Persist the exact checkout request (minus stripeConfirmationToken) in sessionStorage before posting it so resume is possible after a reload. When initializing a session purely to recover, pass voucherCartId on initialize so the session is minted even if the location's catalog has since closed.
closeCheckout
Abandon an unfinished voucher checkout so the customer can start over. Succeeds only once the server has confirmed no booking was, or can still be, created for the attempt; the reservation is released and the cart key is retired so a late retry of the old request cannot book. Returns the resulting state (confirmed means a booking did exist after all).
const closed = await api.voucherRedemption.closeCheckout(cartId);
if (closed.success && closed.data.state === 'resolved') {
// start a fresh cart
}VoucherCheckoutStatus (data)
| Field | Type | Description |
|---|---|---|
state | 'unknown' | 'resume' | 'resolving' | 'resolved' | 'confirmed' | See above |
confirmation | string | Booking confirmation code when confirmed |
paymentIntentId | string | Bound intent to resume with, when resume |
stripeAccountId | string | The connected account the intent lives on |
Full Example: Cart to Checkout
End-to-end flow from creating a cart through successful checkout.
const api = new ResytechApi();
// 1. Initialize
const init = await api.initialization.initialize({
identifier: 'checkout-flow'
});
const activity = init.activities[0];
const equipment = activity.equipment[0];
const duration = activity.durations[0];
// 2. Create cart
const cartResult = await api.cart.createOrUpdateCart({
cart: {
activity: activity.uuid,
duration: duration.uuid,
date: '2025-07-15',
timeSlotStart: '10:00',
timeSlotEnd: '11:00',
equipment: [
{
equipmentUuid: equipment.uuid,
quantity: 1,
seats: 2
}
]
}
});
if (!cartResult.success) {
console.error('Cart failed:', cartResult.message, 'Code:', cartResult.errorCode);
return;
}
const cartId = cartResult.cartId;
console.log('Cart created:', cartId);
console.log('Subtotal:', cartResult.cart.subtotal);
// 3. Update customer info
await api.cart.updateCustomer({
cartId: cartId,
customer: {
fullName: 'Jane Smith',
email: 'jane@example.com',
phone: '5551234567',
countryCode: '1'
}
});
// 4. Apply a coupon
const couponResult = await api.cart.applyCoupon({
cartId: cartId,
couponCode: 'SUMMER20'
});
if (couponResult.success) {
console.log('Coupon applied!');
}
// 5. Check trip protection
const tripPreview = await api.cart.getTripProtectionPreview({
cartId: cartId
});
// 6. Process checkout
// In a real app, collect the Stripe token via Stripe.js/Elements
const checkout = await api.checkout.checkout({
cart: {
activity: activity.uuid,
duration: duration.uuid,
date: '2025-07-15',
timeSlotStart: '10:00',
timeSlotEnd: '11:00',
equipment: [
{ equipmentUuid: equipment.uuid, quantity: 1, seats: 2 }
],
customer: {
fullName: 'Jane Smith',
email: 'jane@example.com',
phone: '5551234567'
},
tripProtectionSelected: tripPreview.available
},
cartId: cartId,
stripeConfirmationToken: 'tok_from_stripe_js',
agreements: ['agreement-uuid-1'],
smsOptIn: true
});
if (checkout.success) {
console.log('Booking confirmed!', checkout.confirmation);
console.log('Total charged:', checkout.cart.total);
} else if (checkout.clientSecret) {
// Handle Stripe 3D Secure or other next actions
// After handling, retry with handledNextAction
console.log('Additional payment verification required');
} else {
console.error('Checkout failed:', checkout.message);
}