Booking System

Build a complete appointment booking flow

This guide walks through building a complete booking system with services, providers, and appointment scheduling.

Overview

You’ll learn how to:

  • Display services and providers
  • Show available time slots
  • Create and confirm reservations
  • Process booking payments

Setup

import { createArkyClient } from 'arky-sdk';

const sdk = createArkyClient({
  businessId: process.env.ARKY_BUSINESS_ID!,
  getToken: () => localStorage.getItem('arky_token'),
  setToken: (token) => localStorage.setItem('arky_token', token),
});

Service Listing

Fetch Services

async function getServices() {
  const result = await sdk.reservation.getServices({
    businessId: sdk.config.businessId,
    status: 'ACTIVE',
    limit: 50
  });

  return result.ok ? result.val.items : [];
}

Service Card Component

function ServiceCard({ service, onSelect }) {
  const formatDuration = (minutes: number) => {
    if (minutes < 60) return `${minutes} min`;
    const hours = Math.floor(minutes / 60);
    const mins = minutes % 60;
    return mins ? `${hours}h ${mins}min` : `${hours}h`;
  };

  return (
    <div className="service-card" onClick={() => onSelect(service)}>
      {service.images?.[0] && (
        <img src={`${service.images[0]}?w=300`} alt={service.name} />
      )}
      <div className="info">
        <h3>{service.name}</h3>
        <p className="description">{service.description}</p>
        <div className="meta">
          <span className="duration">{formatDuration(service.duration)}</span>
          <span className="price">{formatPrice(service.price)}</span>
        </div>
      </div>
      <button>Book Now</button>
    </div>
  );
}

Provider Selection

Fetch Providers for Service

async function getProvidersForService(serviceId: string) {
  const result = await sdk.reservation.getProviders({
    businessId: sdk.config.businessId,
    serviceId,
    limit: 50
  });

  return result.ok ? result.val.items : [];
}

Provider Selector

function ProviderSelector({ serviceId, onSelect }) {
  const [providers, setProviders] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    getProvidersForService(serviceId)
      .then(setProviders)
      .finally(() => setLoading(false));
  }, [serviceId]);

  if (loading) return <div>Loading providers...</div>;

  return (
    <div className="provider-selector">
      <h2>Choose a Provider</h2>

      {/* Any available option */}
      <div
        className="provider-option"
        onClick={() => onSelect(null)}
      >
        <div className="avatar">Any</div>
        <div className="info">
          <h4>Any Available</h4>
          <p>Book with the first available provider</p>
        </div>
      </div>

      {/* Specific providers */}
      {providers.map(provider => (
        <div
          key={provider.id}
          className="provider-option"
          onClick={() => onSelect(provider)}
        >
          <img
            src={`${provider.image}?w=80&h=80&fit=cover`}
            alt={provider.name}
            className="avatar"
          />
          <div className="info">
            <h4>{provider.name}</h4>
            <p>{provider.bio}</p>
          </div>
        </div>
      ))}
    </div>
  );
}

Calendar & Time Slots

Fetch Availability

async function getAvailability(
  providerId: string,
  startDate: string,
  endDate: string
) {
  const result = await sdk.reservation.getProviderWorkingTime({
    businessId: sdk.config.businessId,
    providerId,
    from: startDate,
    to: endDate
  });

  return result.ok ? result.val.slots : [];
}

Date Picker

function DatePicker({ providerId, serviceId, onSelectSlot }) {
  const [selectedDate, setSelectedDate] = useState(new Date());
  const [slots, setSlots] = useState([]);
  const [loading, setLoading] = useState(false);

  // Generate week dates
  const weekDates = useMemo(() => {
    const dates = [];
    for (let i = 0; i < 7; i++) {
      const date = new Date(selectedDate);
      date.setDate(date.getDate() - date.getDay() + i);
      dates.push(date);
    }
    return dates;
  }, [selectedDate]);

  // Fetch slots when date changes
  useEffect(() => {
    setLoading(true);

    const startDate = formatDate(weekDates[0]);
    const endDate = formatDate(weekDates[6]);

    getAvailability(providerId, startDate, endDate)
      .then(setSlots)
      .finally(() => setLoading(false));
  }, [selectedDate, providerId]);

  // Filter slots for selected date
  const daySlots = useMemo(() => {
    const dateStr = formatDate(selectedDate);
    return slots.filter(slot => slot.date === dateStr && slot.available);
  }, [slots, selectedDate]);

  return (
    <div className="date-picker">
      {/* Week navigation */}
      <div className="week-nav">
        <button onClick={() => {
          const prev = new Date(selectedDate);
          prev.setDate(prev.getDate() - 7);
          setSelectedDate(prev);
        }}>
          Previous Week
        </button>
        <button onClick={() => {
          const next = new Date(selectedDate);
          next.setDate(next.getDate() + 7);
          setSelectedDate(next);
        }}>
          Next Week
        </button>
      </div>

      {/* Day selector */}
      <div className="days">
        {weekDates.map(date => (
          <button
            key={date.toISOString()}
            className={isSameDay(date, selectedDate) ? 'selected' : ''}
            onClick={() => setSelectedDate(date)}
          >
            <span className="day-name">
              {date.toLocaleDateString('en', { weekday: 'short' })}
            </span>
            <span className="day-number">{date.getDate()}</span>
          </button>
        ))}
      </div>

      {/* Time slots */}
      <div className="time-slots">
        {loading ? (
          <div>Loading available times...</div>
        ) : daySlots.length === 0 ? (
          <div>No available times for this date</div>
        ) : (
          daySlots.map(slot => (
            <button
              key={slot.startTime}
              className="time-slot"
              onClick={() => onSelectSlot(slot)}
            >
              {formatTime(slot.startTime)}
            </button>
          ))
        )}
      </div>
    </div>
  );
}

function formatDate(date: Date): string {
  return date.toISOString().split('T')[0];
}

function formatTime(timestamp: number): string {
  return new Date(timestamp * 1000).toLocaleTimeString('en', {
    hour: 'numeric',
    minute: '2-digit'
  });
}

Booking Form

Customer Information

function BookingForm({ service, provider, slot, onComplete }) {
  const [customer, setCustomer] = useState({
    firstName: '',
    lastName: '',
    email: '',
    phone: ''
  });
  const [notes, setNotes] = useState('');
  const [promoCode, setPromoCode] = useState('');
  const [quote, setQuote] = useState(null);
  const [loading, setLoading] = useState(false);

  // Get quote on mount
  useEffect(() => {
    sdk.reservation.getQuote({
      businessId: sdk.config.businessId,
      slots: [{
        serviceId: service.id,
        providerId: provider?.id,
        startTime: slot.startTime,
        endTime: slot.endTime
      }],
      promoCode: promoCode || undefined
    }).then(result => {
      if (result.ok) setQuote(result.val);
    });
  }, [promoCode]);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);

    try {
      // Create reservation
      const result = await sdk.reservation.createReservation({
        businessId: sdk.config.businessId,
        slots: [{
          serviceId: service.id,
          providerId: provider?.id,
          startTime: slot.startTime,
          endTime: slot.endTime
        }],
        customer,
        notes,
        promoCode: promoCode || undefined
      });

      if (!result.ok) {
        throw new Error(result.val.message);
      }

      onComplete(result.val);
    } catch (err) {
      alert(err.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <form onSubmit={handleSubmit} className="booking-form">
      <div className="booking-summary">
        <h3>{service.name}</h3>
        {provider && <p>with {provider.name}</p>}
        <p>{formatDateTime(slot.startTime)}</p>
        <p>{formatDuration(service.duration)}</p>
      </div>

      <div className="customer-fields">
        <h3>Your Information</h3>
        <div className="row">
          <input
            placeholder="First Name"
            value={customer.firstName}
            onChange={e => setCustomer({ ...customer, firstName: e.target.value })}
            required
          />
          <input
            placeholder="Last Name"
            value={customer.lastName}
            onChange={e => setCustomer({ ...customer, lastName: e.target.value })}
            required
          />
        </div>
        <input
          type="email"
          placeholder="Email"
          value={customer.email}
          onChange={e => setCustomer({ ...customer, email: e.target.value })}
          required
        />
        <input
          type="tel"
          placeholder="Phone"
          value={customer.phone}
          onChange={e => setCustomer({ ...customer, phone: e.target.value })}
        />
        <textarea
          placeholder="Notes (optional)"
          value={notes}
          onChange={e => setNotes(e.target.value)}
        />
      </div>

      <div className="promo-code">
        <input
          placeholder="Promo code"
          value={promoCode}
          onChange={e => setPromoCode(e.target.value)}
        />
      </div>

      {quote && (
        <div className="price-summary">
          <div className="line">
            <span>{service.name}</span>
            <span>{formatPrice(quote.subtotal)}</span>
          </div>
          {quote.discount > 0 && (
            <div className="line discount">
              <span>Discount</span>
              <span>-{formatPrice(quote.discount)}</span>
            </div>
          )}
          <div className="line total">
            <span>Total</span>
            <span>{formatPrice(quote.total)}</span>
          </div>
        </div>
      )}

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

Payment Processing

For paid services, add Stripe payment:

import { loadStripe } from '@stripe/stripe-js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';

function PaymentBookingForm({ reservation, onComplete }) {
  const stripe = useStripe();
  const elements = useElements();
  const [loading, setLoading] = useState(false);

  async function handlePayment() {
    if (!stripe || !elements) return;

    setLoading(true);

    try {
      const card = elements.getElement(CardElement)!;
      const { paymentMethod, error } = await stripe.createPaymentMethod({
        type: 'card',
        card
      });

      if (error) throw new Error(error.message);

      const result = await sdk.reservation.checkout({
        businessId: sdk.config.businessId,
        reservationId: reservation.id,
        paymentMethod: 'stripe',
        paymentMethodId: paymentMethod.id
      });

      if (!result.ok) {
        throw new Error(result.val.message);
      }

      if (result.val.requiresAction) {
        const { error } = await stripe.confirmCardPayment(
          result.val.clientSecret
        );
        if (error) throw new Error(error.message);
      }

      onComplete(result.val.reservation);
    } catch (err) {
      alert(err.message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="payment-form">
      <h3>Payment</h3>
      <CardElement />
      <button onClick={handlePayment} disabled={loading || !stripe}>
        {loading ? 'Processing...' : 'Pay & Confirm'}
      </button>
    </div>
  );
}

Confirmation

function BookingConfirmation({ reservation }) {
  return (
    <div className="booking-confirmation">
      <div className="success-icon">✓</div>
      <h1>Booking Confirmed!</h1>

      <div className="details">
        <p><strong>Service:</strong> {reservation.service.name}</p>
        <p><strong>Provider:</strong> {reservation.provider.name}</p>
        <p><strong>Date:</strong> {formatDateTime(reservation.startTime)}</p>
        <p><strong>Duration:</strong> {formatDuration(reservation.duration)}</p>
      </div>

      <p className="confirmation-email">
        A confirmation email has been sent to {reservation.customer.email}
      </p>

      <div className="actions">
        <button onClick={() => addToCalendar(reservation)}>
          Add to Calendar
        </button>
        <a href="/bookings">View My Bookings</a>
      </div>
    </div>
  );
}

function addToCalendar(reservation) {
  const start = new Date(reservation.startTime * 1000);
  const end = new Date(reservation.endTime * 1000);

  const icsContent = `BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
DTSTART:${formatICSDate(start)}
DTEND:${formatICSDate(end)}
SUMMARY:${reservation.service.name}
DESCRIPTION:Appointment with ${reservation.provider.name}
END:VEVENT
END:VCALENDAR`;

  const blob = new Blob([icsContent], { type: 'text/calendar' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'appointment.ics';
  a.click();
}

Complete Booking Flow

function BookingPage() {
  const [step, setStep] = useState<'service' | 'provider' | 'time' | 'form' | 'confirm'>('service');
  const [service, setService] = useState(null);
  const [provider, setProvider] = useState(null);
  const [slot, setSlot] = useState(null);
  const [reservation, setReservation] = useState(null);

  return (
    <div className="booking-page">
      {step === 'service' && (
        <ServiceList onSelect={s => {
          setService(s);
          setStep('provider');
        }} />
      )}

      {step === 'provider' && (
        <ProviderSelector
          serviceId={service.id}
          onSelect={p => {
            setProvider(p);
            setStep('time');
          }}
        />
      )}

      {step === 'time' && (
        <DatePicker
          serviceId={service.id}
          providerId={provider?.id}
          onSelectSlot={s => {
            setSlot(s);
            setStep('form');
          }}
        />
      )}

      {step === 'form' && (
        <BookingForm
          service={service}
          provider={provider}
          slot={slot}
          onComplete={r => {
            setReservation(r);
            setStep('confirm');
          }}
        />
      )}

      {step === 'confirm' && (
        <BookingConfirmation reservation={reservation} />
      )}
    </div>
  );
}
Tip

Use bulkSchedule to create recurring availability for providers, then getProviderWorkingTime to fetch available slots.