ResytechResytech Docs

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

FieldTypeRequiredDescription
cartClientShoppingCartYesCart contents (see below)
cartIdstringNoExisting cart ID to update; omit to create new

CreateOrUpdateShoppingCartResponse

FieldTypeDescription
successbooleanWhether the operation succeeded
cartIdstringCart identifier for subsequent operations
cartServerShoppingCartOverviewServer-calculated pricing and line items
errorCodeShoppingCartErrorCodeNumeric error code (if failed)
removalsShoppingCartRemoval[]Items removed due to validation (e.g., sold out equipment)
clientSecretstringStripe PaymentIntent client secret (for deferred payment setup)
operatorAccountIdstringStripe connected account ID

ClientShoppingCart

The core cart object sent to the server. Used by both createOrUpdateCart and checkout.

FieldTypeRequiredDescription
activitystringYesActivity UUID
durationstringYesDuration UUID
datestringYesBooking date (YYYY-MM-DD)
timeSlotStartstringYesStart time (HH:mm)
timeSlotEndstringYesEnd time (HH:mm)
equipmentClientShoppingCartEquipment[]NoEquipment selections
customerClientShoppingCartCustomerNoCustomer info (required for checkout)
downPaymentRequestedbooleanNoRequest down payment instead of full payment
isPrivateTourbooleanNoBook as a private tour. Honoured only when the activity has allowPrivateTours; forced on when it has privateToursOnly.
partySizenumberNoTours: 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.
durationMinsnumberNoDynamic duration in minutes
tripProtectionSelectedbooleanNoOpt into trip protection

ClientShoppingCartEquipment

FieldTypeRequiredDescription
equipmentUuidstringYesEquipment UUID
quantitynumberYesNumber of units
seatsnumberYesNumber of seats (guests)
addonsClientShoppingCartEquipmentAddon[]NoAdd-ons for this equipment

ClientShoppingCartEquipmentAddon

FieldTypeRequiredDescription
addonUuidstringYesAdd-on UUID
quantitynumberYesQuantity
equipmentUuidstringYesEquipment 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

FieldTypeRequiredDescription
fullNamestringNoCustomer full name
emailstringNoEmail address (required for checkout)
countryCodestringNoPhone country code (e.g., "1" for US)
countrystringNoCountry name or code
phonestringNoPhone number

ServerShoppingCartOverview

The server-side cart returned in responses. Contains calculated pricing, fees, and applied discounts.

FieldTypeDescription
lineItemsCartLineItem[]Itemized charges
feesFee[]Taxes and fees
subtotalnumberSubtotal before fees and discounts
totalnumberTotal after all calculations
feesTotalnumberSum of all fees
discountTotalnumberSum of all discounts
paidnumberAmount already paid
balancenumberRemaining balance
couponCodestringApplied coupon code
giftCardCodestringApplied gift card code
giftCardAmountAppliednumberGift card amount used
giftCardBalancenumberRemaining gift card balance
voucherAmountAppliednumberPrepaid value covered by applied Activity Vouchers. Render as a negative row; total does not subtract it
vouchersVoucherSummary[]Applied Activity Vouchers — name, masked suffix, coverage lines. Never the code
amountDuenumbertotal net of gift-card and voucher tender. Server-computed bottom line
dueNownumberAmount due at checkout
dueLaternumberAmount due later (for down payments)
tripProtectionSelectedbooleanWhether trip protection is selected
tripProtectionPricenumberTrip protection price
equipmentServerShoppingCartEquipment[]Confirmed equipment selections

CartLineItem

FieldTypeDescription
namestringLine item description
pricenumberAmount
typestringItem type identifier

Fee

FieldTypeDescription
uuidstringFee identifier
namestringFee display name
amountnumberFee amount
typeTaxesAndFeesType0 = 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

FieldTypeRequiredDescription
customerClientShoppingCartCustomerYesCustomer details
cartIdstringNoCart 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

FieldTypeRequiredDescription
couponCodestringNoCoupon code to apply
cartIdstringNoCart 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

FieldTypeRequiredDescription
giftCardCodestringNoGift card code
cartIdstringNoCart ID

ApplyGiftCardResponse

FieldTypeDescription
successbooleanWhether the operation succeeded
errorCodeGiftCardErrorCodeError code (if failed)
availableBalancenumberRemaining 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

FieldTypeRequiredDescription
cartClientShoppingCartYesCurrent cart
cartIdstringNoCart ID
voucherCodesstring[]YesNew codes to apply (may be empty)
retainedVoucherIdsstring[]NoIds of already-applied vouchers to keep

VoucherSummary

FieldTypeDescription
idstringVoucher id — pass back in retainedVoucherIds
suffixstringLast characters of the code, for "ending 1A2B" copy
namestringOffer name
quantitynumberUnits this voucher covers
prepaidAmountnumberValue it contributes to voucherAmountApplied
coveragestring[]Human-readable lines describing what it covers on this cart
redemptionIdstringSet once the voucher is reserved against a booking
redemptionStatusnumber0 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

FieldTypeRequiredDescription
cartIdstringNoCart ID

TripProtectionPreviewResponse

FieldTypeDescription
availablebooleanWhether trip protection is offered
pricenumberPrice for trip protection
titlestringDisplay title
descriptionstringDisplay description
coverageTypenumberCoverage type identifier
coverageAmountnumberCoverage 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

FieldTypeRequiredDescription
cartClientShoppingCartYesComplete cart with customer info
stripeConfirmationTokenstringNoStripe confirmation token for payment
handledNextActionstringNoStripe PaymentIntent ID after handling 3DS
customFieldsCustomFieldAnswer[]NoAnswers to custom fields
demographicsDemographicValue[]NoGuest demographic breakdown
agreementsstring[]NoUUIDs of accepted agreements
smsOptInbooleanNoWhether customer opts into SMS
cartIdstringNoCart ID
fingerprintstringNoOptional device fingerprint forwarded to fraud checks
voucherCheckoutbooleanNoSet 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
expectedVoucherDueNownumberNoThe 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

FieldTypeDescription
uuidstringCustom field UUID
answerstringCustomer's answer

DemographicValue

FieldTypeDescription
demographicUuidstringDemographic UUID
uuidstringValue UUID
valuenumberCount (e.g., 2 adults, 1 child)

CheckoutResponse

FieldTypeDescription
successbooleanWhether checkout completed
confirmationstringBooking confirmation number
cartServerShoppingCartOverviewFinal cart summary
cartActionCreateOrUpdateShoppingCartResponseCart validation result
clientSecretstringStripe client secret (if further payment action needed)
paymentIntentIdstringStripe 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

FieldTypeDescription
isAvailablebooleanWhether gift card purchasing is enabled
allowFlatAmountsbooleanWhether preset amounts are offered
flatAmountsnumber[]Preset dollar amounts
allowVariableAmountsbooleanWhether custom amounts are allowed
variableMinAmountnumberMinimum custom amount
variableMaxAmountnumberMaximum custom amount
requireRecipientEmailbooleanWhether recipient email is required
allowCustomMessagebooleanWhether a custom message is allowed
expirationDaysnumberNumber 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

FieldTypeRequiredDescription
amountnumberYesGift card amount in dollars
purchaserNamestringNoBuyer's name
purchaserEmailstringNoBuyer's email
purchaserPhonestringNoBuyer's phone
purchaserPhoneCountryCodestringNoPhone country code
recipientNamestringNoRecipient's name
recipientEmailstringNoRecipient's email
customMessagestringNoPersonal message
stripeConfirmationTokenstringNoStripe token for payment
handledNextActionstringNoStripe PaymentIntent ID after 3DS

PurchaseGiftCardResponse

FieldTypeDescription
successbooleanWhether the purchase succeeded
giftCardCodestringThe gift card code
giftCardAmountnumberAmount loaded
expiresAtDateExpiration date
recipientEmailstringWhere the gift card was sent
clientSecretstringStripe client secret (if 3DS required)
paymentIntentIdstringStripe 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

FieldTypeDescription
offerIdstringOffer id — send on purchase
revisionIdstringChanges whenever the operator edits the offer — send on purchase
slugstringOffer slug
timeZonestringIANA time zone of the selling location, for rendering dates in the terms
settingsVoucherOfferSettingsName, description, price, sale window, per-order maximum, cancellation policy and the frozen terms
pricingVoucherPurchasePricingquantity, unitPrice, subtotal, fees, taxes, grandTotal, currency, itemized charges[]
activityVoucherActivityDisplayCurrent name, description and media of the covered activity, for presentation

VoucherOfferTerms (settings.terms)

FieldTypeDescription
activityUuid, activityNamestringThe covered activity
durationUuid, durationMinutesstring, numberThe covered duration
entitlementType1 | 21 equipment rental, 2 admission
quantitynumberUnits covered per voucher
equipmentUuidsstring[]Equipment the voucher may be applied to (any one per unit)
equipmentNamesRecord<string,string>Display names keyed by uuid
includedAddonsVoucherIncludedAddon[]Add-ons included per voucher: { equipmentUuid, addonUuid, name, quantity }
bookingStartsAt, bookingEndsAtstring | DateWhen the customer may make the booking. Null = unrestricted
experienceStartsOn, experienceEndsOnstringRedeemable experience dates, inclusive, YYYY-MM-DD
daysOfWeeknumber[]0 Sunday … 6 Saturday. Empty = any day
excludedWindowsVoucherDateWindow[]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

FieldTypeRequiredDescription
requestIdstringYesFrom newRequestIdentity()
accessTokenstringYesFrom newRequestIdentity(). Authorizes access to this order on retries
offerIdstringYesFrom the quote
revisionIdstringYesFrom the quote
quantitynumberYes1 to settings.maxQuantityPerOrder
expectedTotalnumberYesThe quote's pricing.grandTotal you showed
purchaserNamestringYes
purchaserEmailstringYesReceives the voucher email (every code plus the terms) unless a recipient is given
recipientNamestringNoGift recipient
recipientEmailstringNoGift 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
acceptTermsbooleanYesMust be true
stripeConfirmationTokenstringNoRequired to create a new order. Omit to probe or resume
handledNextActionstringNoPaymentIntent id after 3DS, or to poll a processing payment

VoucherPurchaseResult (data)

FieldTypeDescription
action'success' | 'next_action' | 'processing' | 'payment_error'What to do next
orderVoucherOrderThe order: orderId, paymentStatus, fulfillmentStatus (0 pending, 1 issued, 2 blocked), expiresAt, purchaser/recipient, settings, pricing, vouchers[] ({ id, code, status, paidAmount }), deliveries[]
clientSecretstringSet on next_action
paymentIntentIdstringSet on next_action and processing
messagestringCustomer-facing text on payment_error

VoucherErrorCode

CodeMeaningWhat to do
QuoteChangedOffer or price changed since the quoteDrop the attempt, re-quote
SoldOutThe offer's sales limit is reachedDrop the attempt
OrderExpiredThe unpaid order timed out (20 min) or was releasedMint a new identity
PaymentRequiredProbe with no token found no orderSafe to start over
PaymentMismatchThe payment on file does not match this requestContact the operator
RequestConflict, AccessTokenConflictThe identity is already bound to a different requestMint a new identity
NotFound, Unavailable, OutsideSaleWindow, SaleEnded, InvalidOfferThe offer cannot be sold right nowShow message
InvalidRequest, InvalidPriceValidation failureShow message
PaymentsUnavailable, PaymentUnavailableOperator payments not configured / Stripe unreachableRetry 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:

  1. voucherRedemption.inspect(code) to check the code and learn which activity it covers.
  2. Build a cart for that activity with createOrUpdateCart as usual.
  3. cart.updateVouchers to apply the code(s).
  4. checkout with voucherCheckout: true and expectedVoucherDueNow.

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 null

inspect

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)

FieldTypeDescription
idstringVoucher id
suffixstringLast characters of the code
settingsVoucherOfferSettingsThe offer, including terms
timeZonestringIANA 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)

FieldTypeDescription
state'unknown' | 'resume' | 'resolving' | 'resolved' | 'confirmed'See above
confirmationstringBooking confirmation code when confirmed
paymentIntentIdstringBound intent to resume with, when resume
stripeAccountIdstringThe 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);
}

On this page