Build a Custom Booking Flow
Step-by-step guide to building a complete custom booking experience using the ResytechApi client, from initialization through payment confirmation.
This guide walks through building a complete booking flow from scratch using the API client. Each step includes the API calls, the data you get back, and how to wire them together. You can use this with your own UI framework (React, Vue, Svelte, vanilla JS) or combine it with Resytech web components.
Overview
A complete booking flow has these steps:
- Initialize — load activities, operator info, and auth token
- Select activity — display activities and let the customer choose
- Select equipment — show equipment options for the chosen activity
- Select duration — present available durations
- Select date — load the calendar and pick a date
- Select time slot — load available times for that date
- Select add-ons — load eligible add-ons (optional)
- Build cart — create the server-side cart with all selections
- Collect customer info — name, email, phone
- Checkout — process payment via Stripe
Step 1: Initialize
const api = new ResytechApi({ debug: true });
const init = await api.initialization.initialize({
identifier: 'my-booking-page'
});
if (!init.success) {
showError(init.message);
return;
}
// Everything you need is in the response
const activities = init.activities; // Activity list with equipment, durations, media
const operator = init.operator; // Company name, contact info, address
const settings = init.checkoutSettings; // Theme, custom CSS, tracking IDs
const theme = settings?.activityPageTheme; // Layout and color themeAfter initialization, the API client is authenticated. All subsequent calls include the bearer token automatically.
What you get from init
The activities array contains everything needed to render activity cards, equipment selectors, and duration pickers — no additional API calls required for browsing. Each activity includes:
name,tagline,description— display textmedia[]— images (usethumbnailUrifor cards, fullurifor detail pages)equipment[]— available equipment with capacity, images, add-onsdurations[]— available time durations with UUIDsstartingAtPrice— lowest price for "from $X" displaymanifest— guest limits (guestLimit,guestMinimum,ageRestriction)firstAvailableDate— earliest bookable datetype—0= Equipment Rental,1= Tour
Step 2: Select Activity
Display the activities and let the customer pick one. No API call needed — use the data from init.
activities.forEach(activity => {
const card = document.createElement('div');
card.innerHTML = `
<img src="https://bookit-dashboard-assets.s3.us-east-2.amazonaws.com/${activity.media[0]?.thumbnailUri}" />
<h3>${activity.name}</h3>
<p>${activity.tagline || ''}</p>
<p>From $${activity.startingAtPrice.toFixed(2)}</p>
`;
card.onclick = () => onActivitySelected(activity);
container.appendChild(card);
});Or use the web component:
<resytech-activity-list></resytech-activity-list>document.addEventListener('activity-select', (e) => {
onActivitySelected(e.detail.activity);
});Step 3: Select Equipment
Each activity has an equipment[] array. Show the options and let the customer pick.
function onActivitySelected(activity) {
selectedActivity = activity;
activity.equipment.forEach(eq => {
const card = document.createElement('div');
card.innerHTML = `
<h4>${eq.name}</h4>
<p>${eq.description}</p>
<p>Capacity: ${eq.capacity} guests</p>
`;
card.onclick = () => onEquipmentSelected(eq);
equipmentContainer.appendChild(card);
});
}Single vs. Multi Equipment
Check activity.maxEquipmentPerBooking and activity.allowEquipmentMixing:
maxEquipmentPerBooking === 1— customer picks one equipment typeallowEquipmentMixing === false— only one type allowed, but multiple units- Otherwise — customer can mix different equipment types
Step 4: Select Duration
Durations come from the activity (not the equipment). Show them as buttons.
function onEquipmentSelected(equipment) {
selectedEquipment = equipment;
selectedActivity.durations.forEach(dur => {
const btn = document.createElement('button');
const hours = Math.floor(dur.minutes / 60);
const mins = dur.minutes % 60;
btn.textContent = hours ? `${hours}h${mins ? ` ${mins}m` : ''}` : `${mins}min`;
btn.onclick = () => onDurationSelected(dur);
durationContainer.appendChild(btn);
});
}Dynamic Duration Activities
If activity.dynamicDurationSettings?.enabled is true, the activity uses flexible durations instead of fixed ones. Show a slider or number input:
if (selectedActivity.dynamicDurationSettings?.enabled) {
const settings = selectedActivity.dynamicDurationSettings;
// Show slider: min = settings.minimumHours, max = settings.maximumHours
// Increment = settings.incrementMinutes
// Base rate = settings.baseHourlyRate per hour
}Step 5: Select Date (API Call)
Now we need the API. Fetch the calendar for the selected activity and duration.
async function onDurationSelected(duration) {
selectedDuration = duration;
const result = await api.calendar.getActivityCalendar({
activity: selectedActivity.uuid,
month: new Date().getMonth() + 1,
year: new Date().getFullYear(),
duration: duration.uuid,
includePrices: true
});
if (!result.success) {
showError(result.message);
return;
}
// result.calendar.days is an array of { date, isAvailable, price, blackoutMessage }
result.calendar.days.forEach(day => {
if (day.isAvailable) {
renderAvailableDate(day.date, day.price);
}
});
}For dynamic duration activities, pass durationMins instead of duration:
const result = await api.calendar.getActivityCalendar({
activity: selectedActivity.uuid,
month: 6,
year: 2025,
durationMins: 180, // 3 hours
includePrices: true
});Step 6: Select Time Slot (API Call)
Fetch available time slots for the selected date.
async function onDateSelected(dateString) {
selectedDate = dateString;
const result = await api.timeslots.getActivityTimeSlots({
activity: selectedActivity.uuid,
date: dateString,
duration: selectedDuration.uuid,
equipment: [{
equipmentUuid: selectedEquipment.uuid,
quantity: 1,
seats: 1
}]
});
if (!result.success) {
showError(result.message);
return;
}
// result.timeSlots is an array of { price, remainingSeats, canBookPrivateTour }
result.timeSlots.forEach(slot => {
renderTimeSlot(slot);
});
}Tour Activities
For tour activities (type === 1), the customer selects a guest count rather than equipment quantity. Show remainingSeats on each slot so they know capacity.
Step 7: Select Add-ons (API Call, Optional)
Load eligible add-ons based on the full selection. This step is optional — skip it if the activity has no add-ons.
async function onTimeSlotSelected(slot) {
selectedSlot = slot;
const result = await api.addon.getEligibleAddons({
activity: selectedActivity.uuid,
date: selectedDate,
time: '10:00', // the selected time
duration: selectedDuration.uuid,
equipment: [{
equipmentUuid: selectedEquipment.uuid,
quantity: 1,
seats: 1
}]
});
if (result.success && result.addons?.length) {
result.addons.forEach(addon => {
renderAddon(addon); // { uuid, name, description, price, mediaUri }
});
}
}Step 8: Build Cart (API Call)
Create the server-side cart. This validates availability and returns pricing.
const cartResult = await api.cart.createOrUpdateCart({
cart: {
activity: selectedActivity.uuid,
duration: selectedDuration.uuid,
date: selectedDate,
timeSlotStart: '10:00',
timeSlotEnd: '12:00',
equipment: [{
equipmentUuid: selectedEquipment.uuid,
quantity: 1,
seats: 2,
addons: selectedAddons.map(a => ({
addonUuid: a.uuid,
quantity: 1,
equipmentUuid: selectedEquipment.uuid
}))
}]
}
});
if (!cartResult.success) {
// Check errorCode for specific issues
handleCartError(cartResult.errorCode, cartResult.message);
return;
}
// Store the cart ID for subsequent operations
const cartId = cartResult.cartId;
const serverCart = cartResult.cart; // Pricing breakdown
// Display the price summary
showPriceSummary(serverCart); // subtotal, fees, total, dueNow, dueLaterApply Discounts (Optional)
// Coupon
await api.cart.applyCoupon({ cartId, couponCode: customerCouponInput });
// Gift card
const gcResult = await api.cart.applyGiftCard({ cartId, giftCardCode: customerGiftCardInput });
if (gcResult.success) {
console.log('Remaining balance:', gcResult.availableBalance);
}Trip Protection (Optional)
const preview = await api.cart.getTripProtectionPreview({ cartId });
if (preview.available) {
// Show opt-in to customer: preview.title, preview.description, preview.price
}Step 9: Collect Customer Info
Update the cart with customer details. Email is required for checkout.
await api.cart.updateCustomer({
cartId,
customer: {
fullName: 'Jane Smith',
email: 'jane@example.com',
phone: '5551234567',
countryCode: '1'
}
});Custom Fields
If selectedActivity.customFields has entries, collect those answers:
const customFieldAnswers = selectedActivity.customFields.map(field => ({
uuid: field.uuid,
answer: getCustomFieldValue(field.uuid) // from your form
}));Agreements
If selectedActivity.agreements has entries, show them and collect acceptance:
const acceptedAgreements = selectedActivity.agreements
.filter(a => a.required && userAccepted(a.uuid))
.map(a => a.uuid);Step 10: Checkout with Stripe
Process payment using Stripe. You'll need to:
- Load Stripe.js
- Create a Payment Element or Card Element
- Confirm the payment and get a confirmation token
- Pass the token to the checkout endpoint
<script src="https://js.stripe.com/v3/"></script>// Initialize Stripe with the operator's connected account
const stripe = Stripe('your-publishable-key', {
stripeAccount: cartResult.operatorAccountId
});
// Create and mount a Payment Element
const elements = stripe.elements({
clientSecret: cartResult.clientSecret
});
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// When the customer clicks "Pay"
async function handlePayment() {
// Confirm with Stripe
const { error, confirmationToken } = await stripe.createConfirmationToken({
elements
});
if (error) {
showError(error.message);
return;
}
// Send to Resytech
const result = await api.checkout.checkout({
cart: {
activity: selectedActivity.uuid,
duration: selectedDuration.uuid,
date: selectedDate,
timeSlotStart: '10:00',
timeSlotEnd: '12:00',
equipment: [{
equipmentUuid: selectedEquipment.uuid,
quantity: 1,
seats: 2
}],
customer: {
fullName: 'Jane Smith',
email: 'jane@example.com',
phone: '5551234567'
},
tripProtectionSelected: customerOptedIn
},
cartId,
stripeConfirmationToken: confirmationToken.id,
agreements: acceptedAgreements,
customFields: customFieldAnswers,
smsOptIn: customerOptedInToSms
});
if (result.success) {
// Booking confirmed
showConfirmation(result.confirmation);
} else if (result.clientSecret) {
// 3D Secure or additional verification needed
const { error } = await stripe.handleNextAction({
clientSecret: result.clientSecret
});
if (!error) {
// Retry checkout with the handled payment intent
const retry = await api.checkout.checkout({
...previousRequest,
stripeConfirmationToken: undefined,
handledNextAction: result.paymentIntentId
});
if (retry.success) {
showConfirmation(retry.confirmation);
}
}
} else {
showError(result.message);
}
}Complete Minimal Example
A condensed end-to-end flow for reference:
// Init
const api = new ResytechApi();
const init = await api.initialization.initialize({ identifier: 'my-site' });
const activity = init.activities[0];
const equipment = activity.equipment[0];
const duration = activity.durations[0];
// Calendar
const cal = await api.calendar.getActivityCalendar({
activity: activity.uuid, month: 7, year: 2025,
duration: duration.uuid, includePrices: true
});
const availableDate = cal.calendar.days.find(d => d.isAvailable)?.date;
// Time slots
const slots = await api.timeslots.getActivityTimeSlots({
activity: activity.uuid, date: availableDate,
duration: duration.uuid
});
const slot = slots.timeSlots[0];
// Cart
const cart = await api.cart.createOrUpdateCart({
cart: {
activity: activity.uuid, duration: duration.uuid,
date: availableDate, timeSlotStart: '10:00', timeSlotEnd: '12:00',
equipment: [{ equipmentUuid: equipment.uuid, quantity: 1, seats: 1 }],
customer: { fullName: 'Jane', email: 'jane@example.com' }
}
});
// Checkout (with Stripe token from your payment form)
const result = await api.checkout.checkout({
cart: cart.cart, cartId: cart.cartId,
stripeConfirmationToken: 'tok_xxx'
});
console.log('Confirmed:', result.confirmation);Using Web Components Instead
If you'd rather not build every UI element yourself, use Resytech web components for the display layer and wire them together with events:
<resytech-activity-list></resytech-activity-list>
<resytech-equipment-list id="equipment"></resytech-equipment-list>
<resytech-duration-selector id="duration"></resytech-duration-selector>
<resytech-calendar id="calendar"></resytech-calendar>
<resytech-timeslots id="timeslots"></resytech-timeslots>
<resytech-addon-selector id="addons"></resytech-addon-selector>
<resytech-cart-summary id="cart"></resytech-cart-summary>const api = new ResytechApi();
await api.initialization.initialize({ identifier: 'my-site' });
api.registerComponents();
let selected = {};
document.addEventListener('activity-select', (e) => {
selected.activity = e.detail.activity;
document.getElementById('equipment').activity = selected.activity;
});
document.addEventListener('equipment-select', (e) => {
selected.equipment = e.detail.equipment;
document.getElementById('duration').durations = selected.activity.durations;
});
document.addEventListener('duration-select', (e) => {
selected.duration = e.detail.duration;
const cal = document.getElementById('calendar');
cal.activityId = selected.activity.uuid;
cal.durationId = selected.duration.uuid;
cal.load();
});
document.addEventListener('date-select', (e) => {
selected.date = e.detail.date;
const ts = document.getElementById('timeslots');
ts.activityId = selected.activity.uuid;
ts.date = selected.date;
ts.durationId = selected.duration.uuid;
ts.load();
});
document.addEventListener('timeslot-select', async (e) => {
selected.slot = e.detail.slot;
// Load add-ons
const addons = document.getElementById('addons');
addons.activityId = selected.activity.uuid;
addons.date = selected.date;
addons.time = '10:00';
addons.durationId = selected.duration.uuid;
await addons.load();
// Build cart and show summary
const result = await api.cart.createOrUpdateCart({
cart: {
activity: selected.activity.uuid,
duration: selected.duration.uuid,
date: selected.date,
timeSlotStart: '10:00',
timeSlotEnd: '12:00',
equipment: [{ equipmentUuid: selected.equipment.uuid, quantity: 1, seats: 1 }]
}
});
if (result.success) {
document.getElementById('cart').cart = result.cart;
}
});Or Hand Off to the Hosted Checkout
For the simplest integration, use web components for browsing and hand off to <resytech-booking-embed> for the actual checkout:
document.addEventListener('activity-select', (e) => {
const embed = document.getElementById('booking-embed');
embed.setAttribute('activity-id', e.detail.activity.uuid);
embed.reload();
});See Inline Booking Embed for full details.
Next Steps
- Activities, Calendar & Timeslots — detailed API reference
- Cart Operations & Checkout — cart, coupons, gift cards, Stripe
- Error Handling — error codes and troubleshooting
- Web Components — drop-in UI components
