Skip to main content
POST
/
api
/
calendar
/
v1
/
public
/
bookings
Create Booking
curl --request POST \
  --url https://api.example.com/api/calendar/v1/public/bookings \
  --header 'Content-Type: <content-type>' \
  --header 'x-kordless-key: <x-kordless-key>' \
  --data '
{
  "org_slug": "<string>",
  "service_slug": "<string>",
  "starts_at": "<string>",
  "ends_at": "<string>",
  "timezone": "<string>",
  "contact": {
    "name": "<string>",
    "email": "<string>",
    "phone": "<string>"
  },
  "party_size": 123,
  "host_id": "<string>",
  "team_id": "<string>",
  "notes": "<string>"
}
'
{
  "400": {},
  "401": {},
  "404": {},
  "409": {},
  "422": {},
  "429": {},
  "booking": {
    "id": "<string>",
    "confirmation_number": "<string>",
    "status": "<string>",
    "start_at": "<string>",
    "end_at": "<string>",
    "service_name": "<string>",
    "organization_name": "<string>",
    "location": {}
  },
  "calendar_invite": {
    "ics_url": "<string>"
  }
}

Endpoint

POST https://api.kordless.ai/api/calendar/v1/public/bookings
Creates a new booking for a service. Always check availability before creating a booking to ensure the time slot is open.

Authentication

x-kordless-key
string
required
Your API key
Idempotency-Key
string
Unique key to prevent duplicate bookings on retryExample: booking-user123-1701619200000
Content-Type
string
required
Must be application/json

Request Body

org_slug
string
required
Your organization slug (from your booking page URL)Example: acme-salon
service_slug
string
required
Slug of the service to bookExample: haircut
starts_at
string
required
Start time in ISO 8601 format (UTC)Example: 2025-12-03T14:00:00Z
ends_at
string
required
End time in ISO 8601 format (UTC)Example: 2025-12-03T15:00:00Z
timezone
string
required
IANA timezone name for display purposesExample: America/New_York
contact
object
required
Customer contact information
party_size
integer
Number of people (default: 1)
host_id
string
ID of specific host/staff member (if available in slot)
team_id
string
ID of team (if available in slot)
notes
string
Optional notes or special requests

Response

booking
object
The created booking confirmation
calendar_invite
object
Calendar invite details

Examples

curl -X POST https://api.kordless.ai/api/calendar/v1/public/bookings \
  -H "x-kordless-key: your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: booking-unique-123" \
  -d '{
    "org_slug": "acme-salon",
    "service_slug": "haircut",
    "starts_at": "2025-12-03T14:00:00Z",
    "ends_at": "2025-12-03T15:00:00Z",
    "timezone": "America/New_York",
    "contact": {
      "name": "Jane Doe",
      "email": "[email protected]",
      "phone": "+1234567890"
    },
    "party_size": 1,
    "notes": "First time customer"
  }'

Response Example

{
  "booking": {
    "id": "book_abc123xyz",
    "confirmation_number": "BOOK_ABC123XYZ",
    "status": "confirmed",
    "start_at": "2025-12-03T14:00:00Z",
    "end_at": "2025-12-03T15:00:00Z",
    "service_name": "Haircut",
    "organization_name": "Acme Salon",
    "location": {
      "type": "onsite",
      "value": "123 Main St, Suite 100"
    }
  },
  "calendar_invite": {
    "ics_url": "/api/calendar/v1/public/bookings/book_abc123xyz/invite.ics"
  }
}

Complete Booking Flow

// 1. Get available slots first
const params = new URLSearchParams({
  org_slug: 'acme-salon',
  service_slug: 'haircut',
  from: '2025-12-03T00:00:00Z',
  to: '2025-12-03T23:59:59Z'
});

const slotsResponse = await fetch(
  `https://api.kordless.ai/api/calendar/v1/public/availability?${params}`,
  { headers: { 'x-kordless-key': API_KEY } }
);
const { slots } = await slotsResponse.json();

if (slots.length === 0) {
  throw new Error('No available slots for this date');
}

// 2. Customer selects a slot
const selectedSlot = slots[0];

// 3. Create booking with selected slot
const { booking } = await createBooking({
  org_slug: 'acme-salon',
  service_slug: 'haircut',
  starts_at: selectedSlot.startsAt,
  ends_at: selectedSlot.endsAt,
  timezone: 'America/New_York',
  contact: {
    name: customerName,
    email: customerEmail,
    phone: customerPhone
  }
});

// 4. Show confirmation to customer
showConfirmation({
  confirmationNumber: booking.confirmation_number,
  date: formatDate(booking.start_at),
  time: formatTime(booking.start_at, 'America/New_York'),
  service: booking.service_name,
  location: booking.location.value
});

Best Practices

Prevent duplicate bookings if requests are retried:
const idempotencyKey = `booking-${userId}-${Date.now()}`;

// If this request fails and is retried with the same key,
// it will return the original booking instead of creating a duplicate
The same key returns the same result for 24 hours.
Always check that the slot is available:
const { slots } = await getAvailability({...});

// Verify the slot still exists
const slotAvailable = slots.some(
  s => s.startsAt === selectedStart && s.endsAt === selectedEnd
);

if (!slotAvailable) {
  throw new Error('Selected slot is no longer available');
}
The slot might be booked between checking and creating:
try {
  const booking = await createBooking(data);
  return { success: true, booking };
} catch (error) {
  if (error.status === 409) {
    // Fetch fresh availability
    const newSlots = await getAvailability({...});
    return {
      success: false,
      error: 'That time was just booked',
      alternatives: newSlots.slice(0, 5)
    };
  }
  throw error;
}
Validate email and phone before sending:
function validateContact(contact) {
  if (!contact.name?.trim()) {
    throw new Error('Name is required');
  }

  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(contact.email)) {
    throw new Error('Invalid email format');
  }

  // Phone is optional but validate if provided
  if (contact.phone) {
    const phoneRegex = /^\+?[\d\s-()]+$/;
    if (!phoneRegex.test(contact.phone)) {
      throw new Error('Invalid phone format');
    }
  }
}
Always confirm the booking to the customer:
const { booking } = await createBooking(data);

await sendEmail({
  to: booking.contact.email,
  subject: `Booking Confirmed - ${booking.service_name}`,
  body: `
    Your appointment is confirmed!

    Confirmation #: ${booking.confirmation_number}
    Service: ${booking.service_name}
    Date: ${formatDate(booking.start_at)}
    Time: ${formatTime(booking.start_at)}
    Location: ${booking.location.value}

    To cancel or reschedule, visit:
    https://yoursite.com/manage-booking?confirmation=${booking.confirmation_number}
  `
});

Errors

400
Bad Request
Invalid request data or missing required fields
{
  "detail": "contact.email is required"
}
401
Unauthorized
Invalid or missing API key
{
  "detail": "Missing x-kordless-key header"
}
404
Not Found
Organization or service not found
{
  "detail": "Organization not found"
}
409
Conflict
Time slot no longer available
{
  "detail": "Time slot is no longer available"
}
422
Unprocessable Entity
Business validation failed
{
  "detail": "Booking is outside service hours"
}
429
Too Many Requests
Rate limit exceeded (10 bookings/minute)
Learn more about Error Handling