Skip to content

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:

  1. Service Duration - Actual service time (e.g., 90 minutes for therapy)
  2. Preparation Time - Setup time BEFORE service (e.g., 15 minutes for room preparation)
  3. 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

  1. Qualified Staff Required - Only slots when qualified staff is available
  2. Working Hours - Respects staff schedules and time-off
  3. Conflict Detection - Excludes slots with existing appointments
  4. Skill Matching - Only staff with service skills considered
  5. Load Balancing - Distributed across multiple qualified staff

Endpoint

GET /api/v1/public/availability/grid

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 ObjectId
  • start_date - Cannot be in the past, max 14 days in advance
  • num_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 returned
  • slot_interval_minutes - Spacing between slots
  • availability_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 - Always true (unavailable slots excluded)
  • metadata - Additional context information
  • service_id - Service identifier
  • service_name - Service display name
  • outlet_id - Outlet identifier
  • outlet_name - Outlet display name
  • total_available_slots - Total slots across all days
  • service_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:

  1. Prevents Failed Bookings - Only available slots shown
  2. Reduces Support Load - Fewer "Why can't I book?" inquiries
  3. Improves Conversion - Customers see availability instantly
  4. Prevents Race Conditions - Most conflicts detected before submission
  5. 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:

{
  "detail": "Invalid service ID format"
}

400 Bad Request - Past Date:

{
  "detail": "Cannot check availability for past dates"
}

400 Bad Request - Too Far in Advance:

{
  "detail": "Public API limited to 14 days in advance"
}

400 Bad Request - Invalid Slot Interval:

{
  "detail": "Public API only supports 30 or 60 minute intervals"
}

404 Not Found - Service:

{
  "detail": "Service not found"
}

404 Not Found - Outlet:

{
  "detail": "Outlet not found or inactive"
}

500 Internal Server Error:

{
  "detail": "Failed to generate availability grid: <error details>"
}


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:

  1. After appointment created
  2. After appointment cancelled
  3. After appointment rescheduled
  4. After staff schedule updated
  5. 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.


Public Endpoints

Customer Portal Endpoints

Staff Portal Endpoints

Documentation Pages


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:

  1. Replace SERVICE_ID and OUTLET_ID with your actual values
  2. Host on your business website
  3. Customize styles to match your brand
  4. Add booking form integration

Summary

Key Takeaways

  1. Entry Point for Validation - Always check grid before creating appointments
  2. Buffer Times Included - Slots include prep/cleanup automatically
  3. Display Duration Only - Show service duration to customers, not total blocked time
  4. Refresh Frequently - Re-fetch after bookings or every 5-10 minutes
  5. Handle Empty Days - Display "Unavailable" for days with no slots
  6. No Plan Restrictions - Grid access available to all subscription plans
  7. 14-Day Window - Public API limited to 2 weeks in advance
  8. 30 or 60 Min Intervals - Only supported slot intervals for public API

Next Steps

  1. Review Service Configuration - Ensure buffer times set correctly in Service Management
  2. Test Grid Endpoint - Use the testing scenarios provided above
  3. Integrate Frontend - Follow the React/HTML examples for UI implementation
  4. Set Up Appointment Creation - Connect to Appointment Management endpoints
  5. 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.