Availability Grid (Public)¶
Complete guide to checking multi-day appointment availability using the Public Availability Grid endpoint. This is the entry point for booking validation before creating appointments.
Overview¶
The Availability Grid endpoint provides a calendar view of available appointment time slots across multiple days. This is a critical pre-booking validation step that ensures customers can only select available times.
Key Features:
- Multi-Day Calendar - View availability across 1-14 days
- Real-Time Availability - Respects business hours, staff schedules, and existing bookings
- Buffer Time Handling - Includes preparation and cleanup times automatically
- Conflict Prevention - Filters out unavailable slots from double-bookings
- Smart Slot Generation - 30-minute intervals for customer convenience
- Public Access - No authentication required for public booking widgets
Use Cases:
- Booking Widgets - Embed on business website for customer self-booking
- Mobile Apps - Power availability calendars in customer apps
- Kiosk Systems - Display available slots for walk-in bookings
- Pre-Booking Validation - Validate time slot before appointment creation
Business Rules¶
Buffer Time Implementation (Critical Understanding)¶
The grid uses Option A - Conservative Buffer Handling which includes:
- Service Duration - Actual service time (e.g., 90 minutes for therapy)
- Preparation Time - Setup time BEFORE service (e.g., 15 minutes for room preparation)
- Cleanup Time - Cleanup time AFTER service (e.g., 10 minutes for sanitization)
Total Blocked Time = duration_minutes + preparation_minutes + cleanup_minutes
Example:
Service: Premium Therapy Treatment
- Duration: 90 minutes (actual service)
- Preparation: 15 minutes (room setup)
- Cleanup: 10 minutes (sanitization)
Total Blocked: 115 minutes
How This Affects Available Slots¶
Important: Each slot reserves the FULL blocked time (not just service duration)
Slot at 10:00 AM:
- Start Time: 10:00
- End Time: 11:55 (includes 115 minutes total)
- Customer sees: "10:00 AM - 90 minutes"
- Staff blocked: 10:00 - 11:55 (includes prep/cleanup)
Slot Intervals vs. Blocked Time:
-
Slot Intervals: Remain at 30 minutes for customer convenience
-
Grid shows: 10:00, 10:30, 11:00, 11:30... (30 min apart)
-
Blocked Time: Staff unavailable for full duration + buffers
-
If 10:00 booked, then 10:30/11:00/11:30 become unavailable (overlaps with 10:00-11:55)
This is NOT a bug - This is realistic, professional scheduling that:
- Ensures staff has proper prep/cleanup time
- Prevents rushed services
- Maintains service quality standards
- Reduces operational stress
Booking Window Limits¶
Public API Restrictions:
- Past Dates: Cannot check availability for past dates
- Maximum Advance: 14 days (public API limit)
- Authenticated APIs: 30 days (customer/staff portals)
Subscription Plan Limits:
Appointment creation is subject to subscription plan limits:
| Plan | Max Appointments/Month | Notes |
|---|---|---|
| FREE | 100 | Entry-level businesses |
| PRO | 2,000 | Growing businesses |
| ENTERPRISE | Unlimited | Large organizations |
Note: The grid endpoint itself has no plan restrictions, but creating an appointment will enforce these limits. See Subscription Management for details.
Business Hours Validation¶
The grid respects outlet operating hours:
- Closed days return empty slot arrays
- Slots only generated within opening hours
- Last slot ensures completion before closing time
- Break times automatically excluded from availability
Staff Availability Rules¶
- Qualified Staff Required - Only slots when qualified staff is available
- Working Hours - Respects staff schedules and time-off
- Conflict Detection - Excludes slots with existing appointments
- Skill Matching - Only staff with service skills considered
- Load Balancing - Distributed across multiple qualified staff
Endpoint¶
Authentication: Not required (public access)
Query Parameters¶
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
service_id |
string | ✅ Yes | - | Service ObjectId to check availability for |
outlet_id |
string | ✅ Yes | - | Outlet ObjectId where service will be performed |
start_date |
date | ✅ Yes | - | First date to check (YYYY-MM-DD format) |
num_days |
integer | ❌ No | 7 | Number of days to retrieve (1-14) |
slot_interval_minutes |
integer | ❌ No | 30 | Slot spacing (30 or 60 only) |
Parameter Validation:
service_id- Must be valid MongoDB ObjectId (24-character hex)outlet_id- Must be valid MongoDB ObjectIdstart_date- Cannot be in the past, max 14 days in advancenum_days- Range: 1-14 (public API limit)slot_interval_minutes- Only 30 or 60 allowed (customer convenience)
Example Request¶
curl -X GET "https://api.myreserva.id/api/v1/public/availability/grid?\
service_id=68e63f26241da4ebe30521c8&\
outlet_id=68e4d035886b6f295471fd51&\
start_date=2025-10-14&\
num_days=7&\
slot_interval_minutes=30"
Response Structure¶
{
"start_date": "2025-10-14",
"end_date": "2025-10-20",
"num_days": 7,
"slot_interval_minutes": 30,
"availability_grid": {
"2025-10-14": [
{
"start_time": "09:00",
"end_time": "10:55",
"is_available": true
},
{
"start_time": "11:00",
"end_time": "12:55",
"is_available": true
},
{
"start_time": "14:00",
"end_time": "15:55",
"is_available": true
}
],
"2025-10-15": [],
"2025-10-16": [
{
"start_time": "10:00",
"end_time": "11:55",
"is_available": true
}
],
"2025-10-17": [],
"2025-10-18": [
{
"start_time": "09:00",
"end_time": "10:55",
"is_available": true
}
],
"2025-10-19": [],
"2025-10-20": [
{
"start_time": "13:00",
"end_time": "14:55",
"is_available": true
}
]
},
"metadata": {
"service_id": "68e63f26241da4ebe30521c8",
"service_name": "Premium Therapy Treatment",
"outlet_id": "68e4d035886b6f295471fd51",
"outlet_name": "Downtown Beauty Spa",
"total_available_slots": 5,
"service_duration_minutes": 90
}
}
Response Fields:
start_date- First date in the grid (ISO 8601 format)end_date- Last date in the grid (ISO 8601 format)num_days- Number of days returnedslot_interval_minutes- Spacing between slotsavailability_grid- Dictionary mapping dates to slot arrays- Empty arrays
[]= No availability (closed/fully booked) - Each slot contains:
start_time- Slot start (HH:MM format, 24-hour)end_time- Slot end including buffers (HH:MM format)is_available- Alwaystrue(unavailable slots excluded)
metadata- Additional context informationservice_id- Service identifierservice_name- Service display nameoutlet_id- Outlet identifieroutlet_name- Outlet display nametotal_available_slots- Total slots across all daysservice_duration_minutes- Customer-facing duration (for display)
Frontend Integration Guide¶
Step 1: Fetch Availability Grid¶
async function fetchAvailabilityGrid(serviceId, outletId, startDate, numDays = 7) {
const url = new URL('/api/v1/public/availability/grid', 'https://api.myreserva.id');
url.searchParams.set('service_id', serviceId);
url.searchParams.set('outlet_id', outletId);
url.searchParams.set('start_date', startDate); // YYYY-MM-DD
url.searchParams.set('num_days', numDays);
url.searchParams.set('slot_interval_minutes', 30);
const response = await fetch(url);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to fetch availability');
}
return await response.json();
}
Step 2: Display Calendar View¶
async function displayAvailabilityCalendar(serviceId, outletId) {
try {
// Get today's date in YYYY-MM-DD format
const today = new Date().toISOString().split('T')[0];
// Fetch availability grid
const data = await fetchAvailabilityGrid(serviceId, outletId, today, 7);
// Display service information
console.log(`Service: ${data.metadata.service_name}`);
console.log(`Duration: ${data.metadata.service_duration_minutes} minutes`);
console.log(`Total Available Slots: ${data.metadata.total_available_slots}`);
// Iterate through each day
Object.entries(data.availability_grid).forEach(([date, slots]) => {
console.log(`\n📅 ${formatDate(date)}`);
if (slots.length === 0) {
console.log(' ❌ No availability (closed or fully booked)');
return;
}
// Display available slots
slots.forEach(slot => {
console.log(` ✅ ${formatTime(slot.start_time)}`);
});
});
} catch (error) {
console.error('Failed to load availability:', error.message);
}
}
// Helper functions
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
});
}
function formatTime(timeString) {
const [hours, minutes] = timeString.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour}:${minutes} ${ampm}`;
}
Step 3: Build Interactive UI Component (React Example)¶
import React, { useState, useEffect } from 'react';
function AvailabilityCalendar({ serviceId, outletId }) {
const [grid, setGrid] = useState(null);
const [selectedDate, setSelectedDate] = useState(null);
const [selectedTime, setSelectedTime] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
loadAvailability();
}, [serviceId, outletId]);
async function loadAvailability() {
setLoading(true);
setError(null);
try {
const today = new Date().toISOString().split('T')[0];
const response = await fetch(
`/api/v1/public/availability/grid?` +
`service_id=${serviceId}&` +
`outlet_id=${outletId}&` +
`start_date=${today}&` +
`num_days=7&` +
`slot_interval_minutes=30`
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to load availability');
}
const data = await response.json();
setGrid(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
function handleSlotSelect(date, slot) {
setSelectedDate(date);
setSelectedTime(slot.start_time);
}
function handleBooking() {
if (!selectedDate || !selectedTime) {
alert('Please select a date and time first');
return;
}
// Proceed to appointment creation
// See "Step 4: Create Appointment" below
console.log('Creating appointment:', {
date: selectedDate,
time: selectedTime,
serviceId,
outletId
});
}
if (loading) {
return <div className="loading">Loading availability...</div>;
}
if (error) {
return <div className="error">Error: {error}</div>;
}
if (!grid) {
return <div>No availability data</div>;
}
return (
<div className="availability-calendar">
<div className="service-info">
<h3>{grid.metadata.service_name}</h3>
<p>Duration: {grid.metadata.service_duration_minutes} minutes</p>
<p>Location: {grid.metadata.outlet_name}</p>
</div>
<div className="calendar-grid">
{Object.entries(grid.availability_grid).map(([date, slots]) => (
<div key={date} className="day-column">
<div className="date-header">
{new Date(date).toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
})}
</div>
<div className="slots-container">
{slots.length === 0 ? (
<div className="no-slots">
Unavailable
</div>
) : (
slots.map((slot, index) => (
<button
key={index}
className={`slot-button ${
selectedDate === date && selectedTime === slot.start_time
? 'selected'
: ''
}`}
onClick={() => handleSlotSelect(date, slot)}
>
{formatTime(slot.start_time)}
</button>
))
)}
</div>
</div>
))}
</div>
{selectedDate && selectedTime && (
<div className="booking-summary">
<h4>Selected Time:</h4>
<p>
{new Date(selectedDate).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
{' at '}
{formatTime(selectedTime)}
</p>
<button
className="book-button"
onClick={handleBooking}
>
Book Appointment
</button>
</div>
)}
</div>
);
}
function formatTime(timeString) {
const [hours, minutes] = timeString.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour}:${minutes} ${ampm}`;
}
export default AvailabilityCalendar;
Step 4: Create Appointment (After Slot Selection)¶
IMPORTANT: The availability grid is only for validation - it does NOT create appointments. After the customer selects a slot, you must create an appointment using the appropriate API:
For Public Bookings (Customer Self-Service):
async function createPublicAppointment(appointmentData) {
const response = await fetch('/api/v1/customer/appointments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${customerToken}` // Customer JWT
},
body: JSON.stringify({
outlet_id: appointmentData.outletId,
appointment_date: appointmentData.date, // From grid: "2025-10-14"
start_time: appointmentData.time, // From grid: "09:00"
services: [{
service_id: appointmentData.serviceId,
staff_id: appointmentData.staffId || null, // Optional
price: appointmentData.price,
duration_minutes: appointmentData.duration
}],
notes: appointmentData.notes || ''
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create appointment');
}
return await response.json();
}
For Staff Bookings:
async function createStaffAppointment(appointmentData) {
const response = await fetch('/api/v1/appointments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${staffToken}` // Staff JWT
},
body: JSON.stringify({
customer_id: appointmentData.customerId,
outlet_id: appointmentData.outletId,
appointment_date: appointmentData.date,
start_time: appointmentData.time,
services: [{
service_id: appointmentData.serviceId,
staff_id: appointmentData.staffId,
price: appointmentData.price,
duration_minutes: appointmentData.duration
}],
notes: appointmentData.notes || ''
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to create appointment');
}
return await response.json();
}
See: Appointment Management for complete appointment creation documentation.
Step 5: Handle Errors Gracefully¶
async function handleAvailabilityCheck(serviceId, outletId, startDate) {
try {
const data = await fetchAvailabilityGrid(serviceId, outletId, startDate, 7);
// Check if any slots available
const totalSlots = Object.values(data.availability_grid)
.reduce((sum, slots) => sum + slots.length, 0);
if (totalSlots === 0) {
return {
success: true,
message: 'No available slots found in the selected period. Try selecting a different date range or service.',
data: null
};
}
return {
success: true,
message: `Found ${totalSlots} available slots`,
data: data
};
} catch (error) {
// Handle specific error cases
if (error.message.includes('Invalid service ID')) {
return {
success: false,
message: 'The selected service is not available. Please choose another service.',
data: null
};
}
if (error.message.includes('Outlet not found')) {
return {
success: false,
message: 'The selected location is currently unavailable.',
data: null
};
}
if (error.message.includes('past dates')) {
return {
success: false,
message: 'Cannot check availability for past dates. Please select a current or future date.',
data: null
};
}
if (error.message.includes('14 days')) {
return {
success: false,
message: 'Availability can only be checked up to 14 days in advance.',
data: null
};
}
// Generic error
return {
success: false,
message: 'Unable to load availability at this time. Please try again later.',
data: null
};
}
}
Integration Workflow¶
Complete Booking Flow (6 Steps)¶
1. Customer selects service and outlet
↓
2. Fetch availability grid (this endpoint)
↓
3. Customer selects date and time slot
↓
4. Create appointment (POST /api/v1/customer/appointments)
↓
5. Customer completes payment (if required)
↓
6. Appointment confirmed (webhook or manual)
Critical Points:
- Step 2 (Grid) - Validates slot is available before booking attempt
- Step 4 (Create) - Re-validates availability + additional business rules
- Double Validation - Grid prevents UI errors, create endpoint prevents race conditions
Why Grid is Entry Point for Validation¶
Without Grid Check: ❌ Customer selects random time → appointment creation fails → poor UX
With Grid Check: ✅ Customer only sees available times → appointment creation succeeds → excellent UX
Benefits:
- Prevents Failed Bookings - Only available slots shown
- Reduces Support Load - Fewer "Why can't I book?" inquiries
- Improves Conversion - Customers see availability instantly
- Prevents Race Conditions - Most conflicts detected before submission
- Professional Experience - Industry-standard booking flow
Common Pitfalls & Best Practices¶
❌ DON'T: Show end_time to Customers¶
Problem: The end_time in the grid includes buffer times (prep + cleanup), which customers shouldn't see.
Bad Example:
// ❌ WRONG: Shows "09:00 - 10:55" (includes 25min buffers)
displaySlot(`${slot.start_time} - ${slot.end_time}`);
Good Example:
// ✅ CORRECT: Shows "09:00 - 90 minutes"
displaySlot(slot.start_time, grid.metadata.service_duration_minutes);
Why: Customers care about service duration, not total blocked time.
❌ DON'T: Expect All 30-Min Intervals Available¶
Problem: Treating gaps as bugs instead of intentional scheduling.
Understanding:
09:00 - 10:55 ✅ Available (115min blocked)
09:30 - 11:25 ✅ Available NOW
10:00 - 11:55 ✅ Available NOW
10:30 - 12:25 ❌ Unavailable (conflicts with another booking)
11:00 - 12:55 ✅ Available (first free slot after 09:00 booking)
If customer books 09:00, then 09:30/10:00 disappear (overlap with 09:00-10:55).
This is correct behavior - ensures staff has proper prep/cleanup time.
❌ DON'T: Cache Grid Results Too Long¶
Problem: Stale availability causes double-booking.
Bad Example:
// ❌ WRONG: Cache for 1 hour
const cache = new Map();
function getCachedGrid(serviceId, outletId, date) {
const key = `${serviceId}-${outletId}-${date}`;
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < 3600000) { // 1 hour
return cached.data;
}
// Fetch new data...
}
Good Example:
// ✅ CORRECT: Refresh after bookings or max 5-10 minutes
function getGrid(serviceId, outletId, date) {
// Always fetch fresh data OR
// Cache max 5-10 minutes with aggressive invalidation on bookings
return fetchAvailabilityGrid(serviceId, outletId, date);
}
Why: Availability changes rapidly with bookings. Refresh frequently or after each booking.
✅ DO: Display Total Slots for Transparency¶
function displayAvailabilityMetrics(grid) {
console.log(`Total Available Slots: ${grid.metadata.total_available_slots}`);
// Show per-day breakdown
Object.entries(grid.availability_grid).forEach(([date, slots]) => {
console.log(`${date}: ${slots.length} slots`);
});
}
Why: Helps customers understand availability at a glance.
✅ DO: Handle Empty Days Gracefully¶
function renderDay(date, slots) {
if (slots.length === 0) {
return (
<div className="day-unavailable">
<span className="date">{formatDate(date)}</span>
<span className="message">Fully Booked</span>
</div>
);
}
return (
<div className="day-available">
<span className="date">{formatDate(date)}</span>
{slots.map(slot => renderSlot(slot))}
</div>
);
}
Why: Empty arrays indicate closed days or full bookings - display clearly.
✅ DO: Refresh Grid After Each Booking¶
async function createAppointmentAndRefresh(appointmentData) {
try {
// Create appointment
const appointment = await createPublicAppointment(appointmentData);
// Immediately refresh availability grid
await loadAvailability();
return appointment;
} catch (error) {
console.error('Booking failed:', error);
throw error;
}
}
Why: Removes booked slot from grid instantly, preventing double-booking attempts.
Error Handling¶
Common Error Responses¶
400 Bad Request - Invalid Parameters:
400 Bad Request - Past Date:
400 Bad Request - Too Far in Advance:
400 Bad Request - Invalid Slot Interval:
404 Not Found - Service:
404 Not Found - Outlet:
500 Internal Server Error:
Performance Considerations¶
Caching Strategy¶
Recommended Approach:
class AvailabilityCache {
constructor() {
this.cache = new Map();
this.maxAge = 5 * 60 * 1000; // 5 minutes
}
get(key) {
const entry = this.cache.get(key);
if (!entry) return null;
const age = Date.now() - entry.timestamp;
if (age > this.maxAge) {
this.cache.delete(key);
return null;
}
return entry.data;
}
set(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
invalidate(serviceId, outletId) {
// Invalidate all cache entries for this service/outlet
const pattern = `${serviceId}-${outletId}-`;
for (const key of this.cache.keys()) {
if (key.startsWith(pattern)) {
this.cache.delete(key);
}
}
}
}
const cache = new AvailabilityCache();
async function getCachedAvailability(serviceId, outletId, startDate, numDays) {
const cacheKey = `${serviceId}-${outletId}-${startDate}-${numDays}`;
// Check cache first
const cached = cache.get(cacheKey);
if (cached) {
console.log('Using cached availability');
return cached;
}
// Fetch fresh data
const data = await fetchAvailabilityGrid(serviceId, outletId, startDate, numDays);
// Store in cache
cache.set(cacheKey, data);
return data;
}
Cache Invalidation Triggers:
- After appointment created
- After appointment cancelled
- After appointment rescheduled
- After staff schedule updated
- Maximum 5-10 minutes age
Subscription Plan Considerations¶
Grid Access (No Restrictions)¶
The availability grid endpoint has no subscription plan restrictions:
- FREE Plan - Full access to grid
- PRO Plan - Full access to grid
- ENTERPRISE Plan - Full access to grid
Why: The grid is a pre-booking validation tool that doesn't create appointments. Plan limits are enforced during appointment creation.
Appointment Creation Limits¶
After selecting a slot from the grid, appointment creation is subject to plan limits:
| Plan | Max Appointments/Month |
|---|---|
| FREE | 100 |
| PRO | 2,000 |
| ENTERPRISE | Unlimited |
Error Example (Plan Limit Exceeded):
{
"detail": "Monthly appointment limit exceeded (100/100). Please upgrade your plan to continue booking.",
"current_plan": "FREE",
"usage": {
"appointments_this_month": 100,
"limit": 100
},
"upgrade_available": true
}
See: Subscription Management for complete plan details.
Related Endpoints & Documentation¶
Public Endpoints¶
- POST /api/v1/public/availability/check - Single-day availability check (simpler alternative)
- GET /api/v1/public/business-hours/{outlet_id} - Get outlet operating hours
- GET /api/v1/public/outlets/{outlet_id}/services/{service_id}/staff - List qualified staff for service
Customer Portal Endpoints¶
- POST /api/v1/customer/appointments - Create customer appointment
- GET /api/v1/customer/appointments/availability-grid - Authenticated customer grid (30 days)
Staff Portal Endpoints¶
- POST /api/v1/appointments - Create staff appointment
- GET /api/v1/staff/availability/availability-grid - Staff-specific availability grid
Documentation Pages¶
- Appointment Management - Complete appointment lifecycle documentation
- Subscription Management - Plan limits and features
- Service Management - Service configuration with buffer times
- Availability Management - Staff schedules and working hours
Testing & Validation¶
Test Scenarios¶
1. Basic Grid Retrieval:
curl -X GET "https://api.myreserva.id/api/v1/public/availability/grid?\
service_id=68e63f26241da4ebe30521c8&\
outlet_id=68e4d035886b6f295471fd51&\
start_date=2025-10-14&\
num_days=7"
Expected: Returns 7-day grid with available slots
2. Single Day:
curl -X GET "https://api.myreserva.id/api/v1/public/availability/grid?\
service_id=68e63f26241da4ebe30521c8&\
outlet_id=68e4d035886b6f295471fd51&\
start_date=2025-10-14&\
num_days=1"
Expected: Returns 1-day grid (today only)
3. Past Date (Should Fail):
curl -X GET "https://api.myreserva.id/api/v1/public/availability/grid?\
service_id=68e63f26241da4ebe30521c8&\
outlet_id=68e4d035886b6f295471fd51&\
start_date=2025-01-01&\
num_days=7"
Expected: 400 error - "Cannot check availability for past dates"
4. Too Far in Advance (Should Fail):
curl -X GET "https://api.myreserva.id/api/v1/public/availability/grid?\
service_id=68e63f26241da4ebe30521c8&\
outlet_id=68e4d035886b6f295471fd51&\
start_date=2025-12-31&\
num_days=7"
Expected: 400 error - "Public API limited to 14 days in advance"
5. Invalid Service (Should Fail):
curl -X GET "https://api.myreserva.id/api/v1/public/availability/grid?\
service_id=invalid-service-id&\
outlet_id=68e4d035886b6f295471fd51&\
start_date=2025-10-14&\
num_days=7"
Expected: 400 error - "Invalid service ID format"
6. 60-Minute Intervals:
curl -X GET "https://api.myreserva.id/api/v1/public/availability/grid?\
service_id=68e63f26241da4ebe30521c8&\
outlet_id=68e4d035886b6f295471fd51&\
start_date=2025-10-14&\
num_days=7&\
slot_interval_minutes=60"
Expected: Returns grid with hourly slots (fewer total slots)
Embedded Widget Example¶
Booking Widget for Business Website¶
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Book Appointment - Downtown Beauty Spa</title>
<style>
.booking-widget {
max-width: 900px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 15px;
margin-top: 20px;
}
.day-column {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.date-header {
background: #f5f5f5;
padding: 10px;
text-align: center;
font-weight: bold;
border-bottom: 1px solid #ddd;
}
.slots-container {
padding: 10px;
}
.slot-button {
width: 100%;
padding: 8px;
margin-bottom: 5px;
border: 1px solid #007bff;
background: white;
color: #007bff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.slot-button:hover {
background: #007bff;
color: white;
}
.slot-button.selected {
background: #28a745;
color: white;
border-color: #28a745;
}
.no-slots {
text-align: center;
padding: 20px;
color: #999;
font-style: italic;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.error {
background: #f8d7da;
color: #721c24;
padding: 15px;
border-radius: 4px;
margin-top: 20px;
}
.booking-summary {
margin-top: 30px;
padding: 20px;
background: #e7f3ff;
border-radius: 8px;
text-align: center;
}
.book-button {
background: #28a745;
color: white;
padding: 12px 30px;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
margin-top: 15px;
}
.book-button:hover {
background: #218838;
}
</style>
</head>
<body>
<div class="booking-widget">
<h2>Book Your Appointment</h2>
<p>Select an available date and time to book your appointment.</p>
<div id="calendar-container"></div>
<div id="summary-container"></div>
</div>
<script>
const SERVICE_ID = '68e63f26241da4ebe30521c8';
const OUTLET_ID = '68e4d035886b6f295471fd51';
let selectedDate = null;
let selectedTime = null;
let gridData = null;
async function init() {
await loadAvailability();
}
async function loadAvailability() {
const container = document.getElementById('calendar-container');
container.innerHTML = '<div class="loading">Loading availability...</div>';
try {
const today = new Date().toISOString().split('T')[0];
const url = `https://api.myreserva.id/api/v1/public/availability/grid?` +
`service_id=${SERVICE_ID}&` +
`outlet_id=${OUTLET_ID}&` +
`start_date=${today}&` +
`num_days=7&` +
`slot_interval_minutes=30`;
const response = await fetch(url);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to load availability');
}
gridData = await response.json();
renderCalendar();
} catch (error) {
container.innerHTML = `<div class="error">Error: ${error.message}</div>`;
}
}
function renderCalendar() {
const container = document.getElementById('calendar-container');
let html = '<div class="calendar-grid">';
Object.entries(gridData.availability_grid).forEach(([date, slots]) => {
html += `<div class="day-column">`;
html += `<div class="date-header">${formatDate(date)}</div>`;
html += `<div class="slots-container">`;
if (slots.length === 0) {
html += `<div class="no-slots">Unavailable</div>`;
} else {
slots.forEach(slot => {
const isSelected = selectedDate === date && selectedTime === slot.start_time;
html += `
<button
class="slot-button ${isSelected ? 'selected' : ''}"
onclick="selectSlot('${date}', '${slot.start_time}')"
>
${formatTime(slot.start_time)}
</button>
`;
});
}
html += `</div></div>`;
});
html += '</div>';
container.innerHTML = html;
}
function selectSlot(date, time) {
selectedDate = date;
selectedTime = time;
renderCalendar();
renderSummary();
}
function renderSummary() {
const container = document.getElementById('summary-container');
if (!selectedDate || !selectedTime) {
container.innerHTML = '';
return;
}
container.innerHTML = `
<div class="booking-summary">
<h4>Selected Time:</h4>
<p>
${formatDateLong(selectedDate)} at ${formatTime(selectedTime)}
</p>
<p>
<strong>${gridData.metadata.service_name}</strong><br>
Duration: ${gridData.metadata.service_duration_minutes} minutes
</p>
<button class="book-button" onclick="bookAppointment()">
Continue to Booking
</button>
</div>
`;
}
function bookAppointment() {
// Redirect to appointment creation page or show booking form
alert(`Booking appointment:\nDate: ${selectedDate}\nTime: ${selectedTime}\nService: ${gridData.metadata.service_name}`);
// In production, redirect to booking form:
// window.location.href = `/book?date=${selectedDate}&time=${selectedTime}&service=${SERVICE_ID}&outlet=${OUTLET_ID}`;
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
});
}
function formatDateLong(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
function formatTime(timeString) {
const [hours, minutes] = timeString.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour % 12 || 12;
return `${displayHour}:${minutes} ${ampm}`;
}
// Initialize on page load
init();
</script>
</body>
</html>
Deployment:
- Replace
SERVICE_IDandOUTLET_IDwith your actual values - Host on your business website
- Customize styles to match your brand
- Add booking form integration
Summary¶
Key Takeaways¶
- Entry Point for Validation - Always check grid before creating appointments
- Buffer Times Included - Slots include prep/cleanup automatically
- Display Duration Only - Show service duration to customers, not total blocked time
- Refresh Frequently - Re-fetch after bookings or every 5-10 minutes
- Handle Empty Days - Display "Unavailable" for days with no slots
- No Plan Restrictions - Grid access available to all subscription plans
- 14-Day Window - Public API limited to 2 weeks in advance
- 30 or 60 Min Intervals - Only supported slot intervals for public API
Next Steps¶
- Review Service Configuration - Ensure buffer times set correctly in Service Management
- Test Grid Endpoint - Use the testing scenarios provided above
- Integrate Frontend - Follow the React/HTML examples for UI implementation
- Set Up Appointment Creation - Connect to Appointment Management endpoints
- Monitor Performance - Implement caching strategy for optimal performance
For complete appointment booking documentation, see Appointment Management.
For subscription plan details and limits, see Subscription Management.