ResytechResytech Docs

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:

  1. Initialize — load activities, operator info, and auth token
  2. Select activity — display activities and let the customer choose
  3. Select equipment — show equipment options for the chosen activity
  4. Select duration — present available durations
  5. Select date — load the calendar and pick a date
  6. Select time slot — load available times for that date
  7. Select add-ons — load eligible add-ons (optional)
  8. Build cart — create the server-side cart with all selections
  9. Collect customer info — name, email, phone
  10. 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 theme

After 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 text
  • media[] — images (use thumbnailUri for cards, full uri for detail pages)
  • equipment[] — available equipment with capacity, images, add-ons
  • durations[] — available time durations with UUIDs
  • startingAtPrice — lowest price for "from $X" display
  • manifest — guest limits (guestLimit, guestMinimum, ageRestriction)
  • firstAvailableDate — earliest bookable date
  • type0 = 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 type
  • allowEquipmentMixing === 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, dueLater

Apply 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:

  1. Load Stripe.js
  2. Create a Payment Element or Card Element
  3. Confirm the payment and get a confirmation token
  4. 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

On this page