# Build a Custom Booking Flow (/developers/custom-booking-flow)



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](/developers/web-components).

Overview [#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 [#step-1-initialize]

```javascript
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 [#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
* `type` — `0` = Equipment Rental, `1` = Tour

Step 2: Select Activity [#step-2-select-activity]

Display the activities and let the customer pick one. No API call needed — use the data from init.

```javascript
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:

```html
<resytech-activity-list></resytech-activity-list>
```

```javascript
document.addEventListener('activity-select', (e) => {
  onActivitySelected(e.detail.activity);
});
```

Step 3: Select Equipment [#step-3-select-equipment]

Each activity has an `equipment[]` array. Show the options and let the customer pick.

```javascript
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 [#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 [#step-4-select-duration]

Durations come from the activity (not the equipment). Show them as buttons.

```javascript
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 [#dynamic-duration-activities]

If `activity.dynamicDurationSettings?.enabled` is true, the activity uses flexible durations instead of fixed ones. Show a slider or number input:

```javascript
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) [#step-5-select-date-api-call]

Now we need the API. Fetch the calendar for the selected activity and duration.

```javascript
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`:

```javascript
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) [#step-6-select-time-slot-api-call]

Fetch available time slots for the selected date.

```javascript
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 [#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) [#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.

```javascript
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) [#step-8-build-cart-api-call]

Create the server-side cart. This validates availability and returns pricing.

```javascript
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) [#apply-discounts-optional]

```javascript
// 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) [#trip-protection-optional]

```javascript
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 [#step-9-collect-customer-info]

Update the cart with customer details. Email is required for checkout.

```javascript
await api.cart.updateCustomer({
  cartId,
  customer: {
    fullName: 'Jane Smith',
    email: 'jane@example.com',
    phone: '5551234567',
    countryCode: '1'
  }
});
```

Custom Fields [#custom-fields]

If `selectedActivity.customFields` has entries, collect those answers:

```javascript
const customFieldAnswers = selectedActivity.customFields.map(field => ({
  uuid: field.uuid,
  answer: getCustomFieldValue(field.uuid) // from your form
}));
```

Agreements [#agreements]

If `selectedActivity.agreements` has entries, show them and collect acceptance:

```javascript
const acceptedAgreements = selectedActivity.agreements
  .filter(a => a.required && userAccepted(a.uuid))
  .map(a => a.uuid);
```

Step 10: Checkout with Stripe [#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

```html
<script src="https://js.stripe.com/v3/"></script>
```

```javascript
// 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 [#complete-minimal-example]

A condensed end-to-end flow for reference:

```javascript
// 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 [#using-web-components-instead]

If you'd rather not build every UI element yourself, use [Resytech web components](/developers/web-components) for the display layer and wire them together with events:

```html
<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>
```

```javascript
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 [#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:

```javascript
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](/developers/web-components-embed) for full details.

Next Steps [#next-steps]

* [Activities, Calendar & Timeslots](/developers/activities) — detailed API reference
* [Cart Operations & Checkout](/developers/cart-and-checkout) — cart, coupons, gift cards, Stripe
* [Error Handling](/developers/error-handling) — error codes and troubleshooting
* [Web Components](/developers/web-components) — drop-in UI components
