Skip to main content
Build your own booking interface that matches your brand perfectly while using meetergo for scheduling logic.

Overview

A custom booking flow typically involves:
  1. Display available meeting types
  2. Show available time slots
  3. Collect attendee information
  4. Create the booking
  5. Show confirmation

Step 1: Fetch Meeting Types

Get available meeting types for a user:
async function getMeetingTypes(userId) {
  const response = await fetch(
    'https://api.meetergo.com/v4/meeting-type',
    {
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'x-meetergo-api-user-id': userId
      }
    }
  );

  return response.json();
}

Display Meeting Types

function MeetingTypeSelector({ meetingTypes, onSelect }) {
  return (
    <div className="meeting-types">
      {meetingTypes.map(type => (
        <button
          key={type.id}
          onClick={() => onSelect(type)}
          className="meeting-type-card"
        >
          <h3>{type.name}</h3>
          <p>{type.duration} minutes</p>
          {type.description && <p>{type.description}</p>}
        </button>
      ))}
    </div>
  );
}

Step 2: Display Available Slots

Fetch and display available time slots:
async function getAvailableSlots(userId, meetingTypeId, startDate, endDate) {
  const params = new URLSearchParams({
    startDate,
    endDate,
    meetingTypeId
  });

  const response = await fetch(
    `https://api.meetergo.com/v4/user/${userId}/bookable-windows?${params}`,
    {
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'x-meetergo-api-user-id': userId
      }
    }
  );

  const data = await response.json();
  return data.windows;
}

Group Slots by Day

function groupSlotsByDay(slots) {
  const grouped = {};

  for (const slot of slots) {
    const date = new Date(slot);
    const dayKey = date.toISOString().split('T')[0];

    if (!grouped[dayKey]) {
      grouped[dayKey] = [];
    }
    grouped[dayKey].push(slot);
  }

  return grouped;
}

Calendar Component

function SlotPicker({ slots, onSelect, selectedSlot }) {
  const groupedSlots = groupSlotsByDay(slots);
  const days = Object.keys(groupedSlots).sort();

  return (
    <div className="slot-picker">
      {days.map(day => (
        <div key={day} className="day-column">
          <h4>{formatDate(day)}</h4>
          <div className="slots">
            {groupedSlots[day].map(slot => (
              <button
                key={slot}
                onClick={() => onSelect(slot)}
                className={`slot ${selectedSlot === slot ? 'selected' : ''}`}
              >
                {formatTime(slot)}
              </button>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

function formatDate(dateString) {
  return new Date(dateString).toLocaleDateString('en-US', {
    weekday: 'short',
    month: 'short',
    day: 'numeric'
  });
}

function formatTime(isoString) {
  return new Date(isoString).toLocaleTimeString('en-US', {
    hour: 'numeric',
    minute: '2-digit'
  });
}

Step 3: Collect Attendee Information

function BookingForm({ onSubmit, isLoading }) {
  const [formData, setFormData] = useState({
    email: '',
    firstname: '',
    lastname: '',
    phone: '',
    company: '',
    notes: ''
  });

  const handleChange = (e) => {
    setFormData(prev => ({
      ...prev,
      [e.target.name]: e.target.value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    onSubmit(formData);
  };

  return (
    <form onSubmit={handleSubmit} className="booking-form">
      <div className="form-row">
        <input
          type="email"
          name="email"
          placeholder="Email *"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>

      <div className="form-row form-row-split">
        <input
          type="text"
          name="firstname"
          placeholder="First name"
          value={formData.firstname}
          onChange={handleChange}
        />
        <input
          type="text"
          name="lastname"
          placeholder="Last name"
          value={formData.lastname}
          onChange={handleChange}
        />
      </div>

      <div className="form-row">
        <input
          type="tel"
          name="phone"
          placeholder="Phone"
          value={formData.phone}
          onChange={handleChange}
        />
      </div>

      <div className="form-row">
        <input
          type="text"
          name="company"
          placeholder="Company"
          value={formData.company}
          onChange={handleChange}
        />
      </div>

      <div className="form-row">
        <textarea
          name="notes"
          placeholder="Notes or questions"
          value={formData.notes}
          onChange={handleChange}
          rows={3}
        />
      </div>

      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Booking...' : 'Confirm Booking'}
      </button>
    </form>
  );
}

Step 4: Create the Booking

async function createBooking({ meetingTypeId, start, hostIds, attendee }) {
  const response = await fetch('https://api.meetergo.com/v4/booking', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      meetingTypeId,
      start,
      hostIds,
      attendee: {
        email: attendee.email,
        firstname: attendee.firstname,
        lastname: attendee.lastname,
        phone: attendee.phone,
        customFields: {
          company: attendee.company,
          notes: attendee.notes
        }
      }
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message);
  }

  return response.json();
}

Step 5: Complete Flow Component

function CustomBookingFlow({ userId, hostIds }) {
  const [step, setStep] = useState('select-type');
  const [meetingTypes, setMeetingTypes] = useState([]);
  const [selectedType, setSelectedType] = useState(null);
  const [slots, setSlots] = useState([]);
  const [selectedSlot, setSelectedSlot] = useState(null);
  const [booking, setBooking] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  // Load meeting types on mount
  useEffect(() => {
    getMeetingTypes(userId).then(setMeetingTypes);
  }, [userId]);

  // Load slots when meeting type selected
  useEffect(() => {
    if (selectedType) {
      const today = new Date().toISOString().split('T')[0];
      const nextMonth = new Date();
      nextMonth.setMonth(nextMonth.getMonth() + 1);
      const endDate = nextMonth.toISOString().split('T')[0];

      getAvailableSlots(userId, selectedType.id, today, endDate)
        .then(setSlots);
    }
  }, [selectedType, userId]);

  const handleTypeSelect = (type) => {
    setSelectedType(type);
    setStep('select-time');
  };

  const handleSlotSelect = (slot) => {
    setSelectedSlot(slot);
    setStep('enter-details');
  };

  const handleFormSubmit = async (attendee) => {
    setIsLoading(true);
    setError(null);

    try {
      const result = await createBooking({
        meetingTypeId: selectedType.id,
        start: selectedSlot,
        hostIds,
        attendee
      });
      setBooking(result);
      setStep('confirmation');
    } catch (err) {
      setError(err.message);
      // If slot unavailable, go back to time selection
      if (err.message.includes('no longer available')) {
        setStep('select-time');
        // Refresh available slots
        const today = new Date().toISOString().split('T')[0];
        const nextMonth = new Date();
        nextMonth.setMonth(nextMonth.getMonth() + 1);
        getAvailableSlots(userId, selectedType.id, today, nextMonth.toISOString().split('T')[0])
          .then(setSlots);
      }
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="booking-flow">
      {/* Progress indicator */}
      <div className="steps">
        <span className={step === 'select-type' ? 'active' : ''}>1. Meeting</span>
        <span className={step === 'select-time' ? 'active' : ''}>2. Time</span>
        <span className={step === 'enter-details' ? 'active' : ''}>3. Details</span>
        <span className={step === 'confirmation' ? 'active' : ''}>4. Done</span>
      </div>

      {error && <div className="error">{error}</div>}

      {step === 'select-type' && (
        <MeetingTypeSelector
          meetingTypes={meetingTypes}
          onSelect={handleTypeSelect}
        />
      )}

      {step === 'select-time' && (
        <>
          <button onClick={() => setStep('select-type')}>← Back</button>
          <h2>Select a time for {selectedType.name}</h2>
          <SlotPicker
            slots={slots}
            selectedSlot={selectedSlot}
            onSelect={handleSlotSelect}
          />
        </>
      )}

      {step === 'enter-details' && (
        <>
          <button onClick={() => setStep('select-time')}>← Back</button>
          <h2>Enter your details</h2>
          <p>
            {selectedType.name} on {formatDate(selectedSlot)} at {formatTime(selectedSlot)}
          </p>
          <BookingForm
            onSubmit={handleFormSubmit}
            isLoading={isLoading}
          />
        </>
      )}

      {step === 'confirmation' && (
        <div className="confirmation">
          <h2>Booking Confirmed!</h2>
          <p>
            Your {selectedType.name} is scheduled for{' '}
            {new Date(selectedSlot).toLocaleString()}
          </p>
          <p>A confirmation email has been sent.</p>
          <p className="booking-id">
            Booking ID: {booking.appointmentId}
          </p>
        </div>
      )}
    </div>
  );
}

Styling

.booking-flow {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.steps {
  display: flex;
  justify-content: space-between;
  margin-bottom: 30px;
  padding-bottom: 15px;
  border-bottom: 1px solid #e5e7eb;
}

.steps span {
  color: #9ca3af;
  font-size: 14px;
}

.steps span.active {
  color: #2563eb;
  font-weight: 600;
}

.meeting-type-card {
  display: block;
  width: 100%;
  padding: 20px;
  margin-bottom: 10px;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  background: white;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.2s;
}

.meeting-type-card:hover {
  border-color: #2563eb;
}

.slot-picker {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 20px;
}

.day-column h4 {
  font-size: 14px;
  margin-bottom: 10px;
}

.slot {
  display: block;
  width: 100%;
  padding: 8px;
  margin-bottom: 5px;
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  background: white;
  cursor: pointer;
}

.slot:hover {
  background: #f3f4f6;
}

.slot.selected {
  background: #2563eb;
  color: white;
  border-color: #2563eb;
}

.booking-form input,
.booking-form textarea {
  width: 100%;
  padding: 12px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  font-size: 16px;
}

.form-row {
  margin-bottom: 15px;
}

.form-row-split {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.booking-form button {
  width: 100%;
  padding: 14px;
  background: #2563eb;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  cursor: pointer;
}

.booking-form button:disabled {
  background: #9ca3af;
  cursor: not-allowed;
}

.error {
  padding: 12px;
  background: #fef2f2;
  color: #dc2626;
  border-radius: 6px;
  margin-bottom: 20px;
}

.confirmation {
  text-align: center;
  padding: 40px 20px;
}

Best Practices

Handle race conditions - Slots can be booked while the user fills the form
Show loading states - Provide feedback during API calls
Validate client-side - Check required fields before API call
Allow going back - Let users change their selections
Display timezone - Show times in the user’s local timezone