API

Bookings - Public & Guest

Public booking endpoints for guests and tenant customers (no auth required).

Base path: /api/v1/bookings

Used by: cms-booki-web-app · Role: guest (public)
organizationId: Resolved by tenant middleware (resolveRequiredTenant) — from x-tenant-slug header (auto-injected by $api plugin from subdomain) or from the :organizationId route param

Public booking endpoints allow guests to create and verify bookings without logging in. All requests are tenant-scoped via subdomain or header.


POST /api/v1/bookings/type/:type — Create Initial Booking (Tenant Subdomain)

Auth: Public (resolveRequiredTenant)
Used by: cms-booki-web-app · Role: guest
organizationId: From x-tenant-slug header or subdomain (tenant middleware)
Description: Create a new booking on the current tenant's services as a guest. If guest email matches an existing customer, that customer account is used instead.

Request

POST /api/v1/bookings/type/haircut
Header: X-Tenant-Slug: janes-salon  (or via subdomain)

Body:

{
  "packageId": "507f1f77bcf86cd799439031",
  "bookingDate": "2026-04-10",
  "bookingTime": "14:30",
  "firstName": "John",
  "lastName": "Smith",
  "email": "john@example.com",
  "phone": "09161234567",
  "preferredPaymentMethod": "cash",
  "password": ""
}

Fields:

  • packageId (string, required): Service package ID (24-hex)
  • bookingDate (string, required): Format YYYY-MM-DD (future date only)
  • bookingTime (string, required): Format HH:MM (24-hour, future time only)
  • firstName (string, required): Guest's first name (max 255)
  • middleName (string, optional): Guest's middle name (max 255)
  • lastName (string, required): Guest's last name (max 255)
  • email (string, required): Valid email (lowercase)
  • phone (string, required): Phone number (10-20 chars)
  • preferredPaymentMethod (string, optional): cash or maya (default: business preference)
  • password (string, optional): If set, creates guest as registered customer

Response (201 Created)

{
  "booking": {
    "_id": "507f1f77bcf86cd799439041",
    "organizationId": "507f191e810c19729de860ea",
    "packageId": "507f1f77bcf86cd799439031",
    "customerId": "507f1f77bcf86cd799439051",
    "bookingDate": "2026-04-10",
    "bookingTime": "14:30",
    "status": "pending",
    "preferredPaymentMethod": "cash",
    "createdAt": "2026-04-01T09:15:00Z",
    "verificationToken": "VERIFY_TOKEN_ABC123..."
  },
  "message": "Booking created. Please verify via email link."
}

Error Responses

400 Bad Request — Past date:

{
  "statusCode": 400,
  "message": "Please select a valid date for your booking. Past dates are not available."
}

400 Bad Request — Invalid package:

{
  "statusCode": 400,
  "message": "Package not found"
}

404 Not Found — Tenant not found:

{
  "statusCode": 404,
  "message": "Organization not found"
}

cURL Example

curl -X POST http://localhost:4001/api/v1/bookings/type/haircut \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Slug: janes-salon" \
  -d '{
    "packageId": "507f1f77bcf86cd799439031",
    "bookingDate": "2026-04-10",
    "bookingTime": "14:30",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "phone": "09161234567",
    "preferredPaymentMethod": "cash"
  }'

POST /api/v1/bookings/organizations/:organizationId/type/:type — Create Booking (Explicit Org ID)

Auth: Public (resolveRequiredTenant + resolveRequiredBranch)
Used by: cms-booki-web-app (when subdomain is not available)
organizationId: From the :organizationId route parameter — no X-Tenant-Slug header needed
branchId: Resolved from X-Branch-Slug header — required for branch resolution
Description: Create a booking by explicitly providing the organization ID in the URL path. Use this when a tenant subdomain is unavailable (e.g., direct API integrations, QR codes, or link-based booking flows).

Request

POST /api/v1/bookings/organizations/507f191e810c19729de860ea/type/haircut
Header: X-Branch-Slug: main-branch

Body: same fields as /type/:type above.

Response (201 Created)

Same as above.

cURL Example

curl -X POST "http://localhost:4001/api/v1/bookings/organizations/507f191e810c19729de860ea/type/haircut" \
  -H "Content-Type: application/json" \
  -H "X-Branch-Slug: main-branch" \
  -d '{
    "packageId": "507f1f77bcf86cd799439031",
    "bookingDate": "2026-04-10",
    "bookingTime": "14:30",
    "firstName": "John",
    "lastName": "Smith",
    "email": "john@example.com",
    "phone": "09161234567"
  }'

Error Responses

422 Unprocessable Entity — Missing or unresolvable branch:

{
  "statusCode": 422,
  "message": "Branch could not be determined. Please ensure you access via a valid branch URL."
}

PUT /api/v1/bookings/:id/verify — Verify Booking

Auth: Public
Used by: cms-booki-web-app (email verification link) · Role: guest
organizationId: Not required (booking ID is sufficient)
Description: Verify a booking from the email confirmation link. No request body is required — the booking is identified by the :id path parameter.

Request

PUT /api/v1/bookings/507f1f77bcf86cd799439041/verify

Response (200 OK)

{
  "booking": {
    "_id": "507f1f77bcf86cd799439041",
    "organizationId": "507f191e810c19729de860ea",
    "status": "confirmed",
    "bookingDate": "2026-04-10",
    "bookingTime": "14:30",
    "verifiedAt": "2026-04-01T09:30:00Z"
  },
  "message": "Booking verified successfully"
}

Error Responses

400 Bad Request — Invalid token:

{
  "statusCode": 400,
  "message": "Invalid or expired verification token"
}

cURL Example

curl -X PUT "http://localhost:4001/api/v1/bookings/507f1f77bcf86cd799439041/verify"

GET /api/v1/bookings/:id — Get Booking by ID

Auth: Public
Used by: cms-booki-web-app · Role: guest
organizationId: Not required (booking ID is sufficient)
Description: Retrieve booking details by ID (no authentication needed).

Request

GET /api/v1/bookings/507f1f77bcf86cd799439041

Response (200 OK)

{
  "_id": "507f1f77bcf86cd799439041",
  "organizationId": "507f191e810c19729de860ea",
  "packageId": "507f1f77bcf86cd799439031",
  "customerId": "507f1f77bcf86cd799439051",
  "bookingDate": "2026-04-10",
  "bookingTime": "14:30",
  "status": "confirmed",
  "preferredPaymentMethod": "cash",
  "createdAt": "2026-04-01T09:15:00Z",
  "verifiedAt": "2026-04-01T09:30:00Z"
}

Error Responses

404 Not Found — Booking doesn't exist:

{
  "statusCode": 404,
  "message": "Booking not found"
}

cURL Example

curl -X GET http://localhost:4001/api/v1/bookings/507f1f77bcf86cd799439041

GET /api/v1/bookings/fully-booked-slots — List Fully Booked Timeslots

Auth: Public (resolveRequiredTenant)
Used by: cms-booki-web-app · Role: guest
organizationId: From x-tenant-slug header or subdomain (tenant middleware)
Description: Get all timeslots that are fully booked for the tenant.

Request

GET /api/v1/bookings/fully-booked-slots
Header: X-Tenant-Slug: janes-salon

Response (200 OK)

{
  "fullyBookedSlots": [
    {
      "date": "2026-04-10",
      "time": "14:30",
      "packageId": "507f1f77bcf86cd799439031"
    },
    {
      "date": "2026-04-10",
      "time": "15:00",
      "packageId": "507f1f77bcf86cd799439031"
    }
  ],
  "count": 2
}

cURL Example

curl -X GET "http://localhost:4001/api/v1/bookings/fully-booked-slots" \
  -H "X-Tenant-Slug: janes-salon"

Notes

  • All public bookings require a valid tenant (subdomain or header).
  • Guest bookings start in PENDING status until email verification.
  • If guest email matches existing tenant customer, customer context is used.
  • Past dates/times are rejected by server validation.
  • See booki-api/src/validations/booking.validation.ts for exact fields.