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.