Bjuti API Documentation

Welcome to the official Bjuti Developer Portal. This documentation covers the REST API and Model Context Protocol (MCP) daemon interfaces. Software developers and autonomous AI agents can search salons and beauty professionals, query pricelists and real-time appointment availability, initiate temporary reservations (15-min On Hold), and confirm bookings via email verification codes.

Official Domains: All API and MCP endpoints are hosted strictly at https://api.bjuti.ee. The main consumer portal is accessible at bjuti.ee. Machine-readable specification is available at /v1/openapi.json.

AI Agent Booking Workflow Order & Parameter Chaining

To finalize an appointment booking reliably, autonomous AI agents and integrations MUST invoke endpoints in the following strictly chained sequence (Steps 1 to 7). Each subsequent step's input parameters are directly sourced from preceding API responses:

7-Step Booking Lifecycle:

  1. Step 1: Authenticate Agent (POST /v1/agent/auth/token or MCP bjuti_agent_auth)
    Input: client_id, client_secret
    ➔ Sourced Output: access_token (Bearer token valid for strictly 15 minutes / 900 seconds) ➔ Pass in all subsequent requests: Authorization: Bearer <access_token>.
  2. Step 2 (Optional): Discover Service Categories (GET /v1/agent/booking/service-groups or MCP bjuti_get_service_categories)
    ➔ Sourced Output: category_id, name (Hair, Nails, Brows/Lashes, Skin, Massage, etc.).
  3. Step 3: Search Salons & Specialists (GET /v1/agent/booking/nearby-salons or /search-salons or MCP bjuti_find_salons)
    Input: GPS coordinates (lat, lng, radius_km) OR text query (q, salon_name, specialist_name)
    ➔ Sourced Outputs:
    • salons[].location_id ➔ Mandatory input for Steps 4, 5, and 6!
    • salons[].professionals[].professional_id ➔ Mandatory input for Steps 5 and 6!
  4. Step 4: Fetch Salon Pricelist & Services (GET /v1/agent/booking/pricelist or MCP bjuti_get_salon_pricelist)
    Input: location_id (Sourced from Step 3: salons[].location_id)
    ➔ Sourced Outputs:
    • pricelist[].service_id ➔ Mandatory input (array or comma-separated) for Steps 5 and 6!
    • pricelist[].specialists[].professional_id ➔ Specialist confirmation.
  5. Step 5: Query Open Availability Slots (4-Day Window) (GET /v1/agent/booking/availability-slots or MCP bjuti_get_available_slots)
    Inputs:
    • location_id ➔ Sourced from Step 3
    • professional_id ➔ Sourced from Step 3 or 4
    • service_ids ➔ Sourced from Step 4 (comma-separated UUIDs)
    • date ➔ Desired starting date in YYYY-MM-DD
    ➔ Sourced Output: From the slots[date][] array, select client's chosen start_time (e.g. "14:30") ➔ Pass to Step 6! (If booked, check first_available_date and waitlist_available).
  6. Step 6: Temporary Hold (15-Minute Reservation) (POST /v1/agent/booking/hold or MCP bjuti_hold_booking)
    Inputs:
    • location_id (Sourced from Step 3)
    • professional_id (Sourced from Step 3 or 4)
    • service_ids (Sourced from Step 4)
    • date (Sourced from Step 5)
    • start_time (Sourced from Step 5: slots[date][])
    • Client details: contact_firstname, contact_lastname, contact_phone, contact_email, message_text
    ➔ Sourced Output: booking_id (UUID) ➔ Pass to Step 7! Client receives a 6-digit confirmation code via email.
  7. Step 7: Confirm Booking with Email Code (POST /v1/agent/booking/confirm or MCP bjuti_confirm_booking)
    Inputs:
    • booking_id ➔ Sourced from Step 6: booking_id
    • verification_code ➔ 6-digit code provided by client from email
    ➔ Outcome: Appointment confirmed (status: "confirmed") and permanently written to salon calendar!
Alternative / Fallback Workflows:
  • Early Hold Release: POST /v1/agent/booking/release-hold requires booking_id (from Step 6) if the client cancels before code entry.
  • Join Waitlist: POST /v1/agent/booking/waitlist if Step 5 has no open slots and waitlist_available: true.
  • Active Appointments Check: GET /v1/agent/booking/my-active checks client's existing bookings before Step 6 (to prevent 1-booking-per-category conflicts).

Authentication and Token Lifetimes (15-Minute Rule)

All authenticated API endpoints require a Bearer token in the HTTP Authorization header:

HTTP Request Header
Authorization: Bearer <access_token>
Strict Security Policy: Issued access_token tokens are valid for exactly 15 minutes (900 seconds). AI agents or clients must persist the issued refresh_token and request a new access token via /v1/agent/auth/refresh prior to expiration. Authentication endpoints are rate-limited to 10 requests per minute.

Booking & Hold Business Rules

  1. Pricelist-based selection: When booking, the agent must submit service IDs (service_ids) derived directly from the salon pricelist (/pricelist).
  2. 15-Minute On Hold reservation: Initiating a booking (/hold) locks the selected slot with status On Hold. The slot is blocked for other clients and agents for 15 minutes.
  3. Email verification code: Creating a hold triggers a 6-digit confirmation code dispatched to the client's email address. To finalize the booking, submit the code via /confirm (up to 5 attempts before the reservation is cancelled).
  4. 1 active booking per category per client: An AI agent cannot place more than one active booking in the same service category (e.g. Hair) for the same client. If the client already holds a confirmed or held appointment, the system returns error code ACTIVE_BOOKING_EXISTS.

1. Obtain Access Token

POST /v1/agent/auth/token
Public / Rate Limited

Authenticates an AI agent or integration client using credentials and returns a 15-minute Bearer access token along with a refresh token.

Parameters (JSON body)

FieldTypeDescription
client_id requiredstringPublic agent Client ID (e.g. ag_live_9b4c81...)
client_secret requiredstringSecret key for the agent
Request (cURL)
curl -X POST https://api.bjuti.ee/v1/agent/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "ag_live_9b4c81a2f0e84",
    "client_secret": "sec_f20a91e847c1..."
  }'
HTTP 200 OK
{
  "status": "success",
  "data": {
    "access_token": "agt_4f89b1c20e4a779d...",
    "token_type": "Bearer",
    "expires_in": 900,
    "expires_at": "2026-11-08T21:15:00+02:00",
    "refresh_token": "refr_d8a1c902b4e7..."
  }
}
HTTP 401 Unauthorized
{
  "status": "error",
  "code": "INVALID_CREDENTIALS",
  "message": "Invalid client_id or client_secret"
}

2. Refresh Access Token

POST /v1/agent/auth/refresh
Public / Rate Limited

Exchanges a valid refresh token for a fresh 15-minute access token. The old refresh token is invalidated immediately (refresh token rotation).

Parameters (JSON body)

FieldTypeDescription
refresh_token requiredstringRefresh token received from a prior token request
Request (cURL)
curl -X POST https://api.bjuti.ee/v1/agent/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "refr_d8a1c902b4e7..."
  }'
HTTP 200 OK
{
  "status": "success",
  "data": {
    "access_token": "agt_99c301fa882b...",
    "token_type": "Bearer",
    "expires_in": 900,
    "expires_at": "2026-11-08T21:30:00+02:00",
    "refresh_token": "refr_66ea01449bc1..."
  }
}

3. Verify Agent Profile

GET /v1/agent/auth/me
Bearer Token

Returns the identity, permissions, and active token expiration timestamp for the authenticated agent.

Parameters

No query parameters required. Header Authorization: Bearer <token> is required.

Request (cURL)
curl -X GET https://api.bjuti.ee/v1/agent/auth/me \
  -H "Authorization: Bearer agt_4f89b1c20e4a779d..."
HTTP 200 OK
{
  "status": "success",
  "data": {
    "agent_id": "9a148c3b-5521-4d30-bfae-1280ea9010ab",
    "name": "Concierge Booking Bot",
    "email": "bot@example.com",
    "token_expires_at": "2026-11-08T21:15:00+02:00",
    "seconds_remaining": 842
  }
}

4. Service Categories

GET /v1/agent/booking/service-groups
Bearer Token

Returns the main categories of beauty treatments (e.g. Hair, Nails, Brows & Lashes, Massage, Facial).

Query Parameters

ParameterTypeDescription
country optionalstringCountry code (defaults to EE)
Request (cURL)
curl -X GET "https://api.bjuti.ee/v1/agent/booking/service-groups?country=EE" \
  -H "Authorization: Bearer <access_token>"
HTTP 200 OK
{
  "status": "success",
  "data": [
    { "id": "hair", "code": "hair", "name": "Hairdressing", "icon": "scissors" },
    { "id": "nails", "code": "nails", "name": "Nail Services", "icon": "hand-sparkles" },
    { "id": "face", "code": "face", "name": "Facial Care", "icon": "spa" },
    { "id": "brows", "code": "brows", "name": "Brows & Lashes", "icon": "eye" }
  ]
}

5. Search Nearby Salons (GPS)

GET /v1/agent/booking/nearby-salons
Bearer Token

Finds active salon locations based on client geographic coordinates (latitude and longitude) and a search radius using the Haversine formula.

Query Parameters

ParameterTypeDescription
lat requiredfloatClient latitude (e.g. 59.43696)
lng requiredfloatClient longitude (e.g. 24.75353)
radius_km optionalfloatSearch radius in km (defaults to 25)
category optionalstringService category code filter
Request (cURL)
curl -X GET "https://api.bjuti.ee/v1/agent/booking/nearby-salons?lat=59.4369&lng=24.7535&radius_km=10" \
  -H "Authorization: Bearer <access_token>"
HTTP 200 OK
{
  "status": "success",
  "data": [
    {
      "location_id": "96fb29cf-a031-4ce8-b541-ef12998a1234",
      "company_name": "Studio Elegance OÜ",
      "location_name": "Studio Elegance Downtown",
      "address": "Rotermanni 8, Tallinn",
      "distance_km": 0.42,
      "rating": 4.9,
      "coordinates": { "lat": 59.4385, "lng": 24.7561 }
    }
  ]
}

7. Salon Pricelist & Service Add-ons

GET /v1/agent/booking/pricelist
Bearer Token

Returns base treatments available at the given location, prices (VAT incl.), duration in minutes, and available add-ons (addons) associated with each service. Agents can submit both base and add-on service IDs in a single booking request.

Query Parameters

ParameterTypeSource & Description
location_id requireduuidSourced from Step 3 /nearby-salons or /search-salons response field salons[].location_id.
professional_id optionaluuidSourced from Step 3 salons[].professionals[].professional_id to filter treatments by master.
Request (cURL)
curl -X GET "https://api.bjuti.ee/v1/agent/booking/pricelist?location_id=96fb29cf-a031-4ce8-b541-ef12998a1234" \
  -H "Authorization: Bearer <access_token>"
HTTP 200 OK
{
  "status": "success",
  "data": {
    "location_id": "96fb29cf-a031-4ce8-b541-ef12998a1234",
    "salon_name": "Studio Elegance Downtown",
    "services": [
      {
        "service_id": "c14d9b2e-7412-4aa3-8fa9-994b2a8901aa",
        "category": "hair",
        "name": "Women's Haircut & Blow-dry",
        "description": "Includes washing, scalp massage, haircut, and styling.",
        "duration_minutes": 60,
        "price": 45.00,
        "addons": [
          {
            "service_id": "8b14da21-998b-4b11-9a40-ef81203491ba",
            "category": "hair",
            "name": "Intensive Moisture Hair Mask",
            "description": "Deep restructuring treatment powered by K18 or Olaplex.",
            "duration_minutes": 20,
            "price": 15.00
          },
          {
            "service_id": "fe1023ba-1200-4781-a891-cb9041289190",
            "category": "hair",
            "name": "Relaxing Scalp Massage with Essential Oils",
            "duration_minutes": 15,
            "price": 10.00
          }
        ]
      }
    ]
  }
}

8. Availability Slots (4-Day Window & First Available Date)

GET /v1/agent/booking/availability-slots
Bearer Token

Calculates open booking slots across the professional's working schedule for the total duration of all selected services and add-ons:

  • 4-Day Window: Returns open slots from the requested date up to 3 days ahead (date to date + 3 days).
  • First Available Date: If no slots are found within the 4-day window, the system automatically locates the earliest upcoming slot (up to 45 days ahead) in first_available.
  • Waiting List Readiness: waitlist_available: true indicates that if none of the slots suit the client, the client can be placed on the waitlist (/waitlist).

Query Parameters

ParameterTypeSource & Description
location_id requireduuidSourced from Step 3 Salon location UUID (salons[].location_id).
professional_id requireduuidSourced from Step 3 or 4 Professional UUID (salons[].professionals[].professional_id or pricelist[].specialists[].professional_id).
service_ids requiredstringSourced from Step 4 Comma-separated service and addon UUIDs (pricelist[].service_id).
date requiredstringStarting date in format YYYY-MM-DD for the 4-day availability window.
Request (cURL)
curl -X GET "https://api.bjuti.ee/v1/agent/booking/availability-slots?location_id=96fb29cf-...&professional_id=8a1e47b2-...&service_ids=c14d9b2e-...,8b14da21-...&date=2026-11-15" \
  -H "Authorization: Bearer <access_token>"
HTTP 200 OK
{
  "status": "success",
  "data": {
    "location_id": "96fb29cf-a031-4ce8-b541-ef12998a1234",
    "professional_id": "8a1e47b2-3c10-482a-99cd-482b991a0021",
    "requested_date": "2026-11-15",
    "total_duration_minutes": 80,
    "dates": [
      {
        "date": "2026-11-15",
        "day_of_week": "Sunday",
        "slots_count": 0,
        "slots": []
      },
      {
        "date": "2026-11-16",
        "day_of_week": "Monday",
        "slots_count": 2,
        "slots": ["10:00", "14:30"]
      },
      {
        "date": "2026-11-17",
        "day_of_week": "Tuesday",
        "slots_count": 3,
        "slots": ["11:00", "13:00", "16:00"]
      },
      {
        "date": "2026-11-18",
        "day_of_week": "Wednesday",
        "slots_count": 0,
        "slots": []
      }
    ],
    "first_available": null,
    "waitlist_available": true
  }
}

9. Initiate Booking & 15-Minute Hold

POST /v1/agent/booking/hold
Bearer Token

Locks the selected slot for exactly 15 minutes with status On Hold. Enforces the one-active-booking-per-category rule and dispatches a 6-digit confirmation code to the client's email address.

Parameters (JSON body)

FieldTypeSource & Description
location_id requireduuidSourced from Step 3 Salon location UUID.
professional_id requireduuidSourced from Step 3 or 4 Specialist UUID.
service_ids requiredarray[uuid]Sourced from Step 4 Array of treatment UUIDs from pricelist.
date requiredstringSourced from Step 5 Chosen appointment date in YYYY-MM-DD.
start_time requiredstringSourced from Step 5 Open slot start time from slots[date][] (e.g. "14:00").
contact_firstname requiredstringClient first name.
contact_lastname optionalstringClient last name.
contact_phone requiredstringClient mobile phone number.
contact_email requiredstringClient email (where 6-digit confirmation code will be dispatched).
message_text optionalstringOptional note or request for the specialist.
result_photo_base64 optionalstringDesired style/result reference photo (Base64).
Request (cURL)
curl -X POST https://api.bjuti.ee/v1/agent/booking/hold \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "location_id": "96fb29cf-a031-4ce8-b541-ef12998a1234",
    "professional_id": "8a1e47b2-3c10-482a-99cd-482b991a0021",
    "service_ids": ["c14d9b2e-7412-4aa3-8fa9-994b2a8901aa"],
    "date": "2026-11-15",
    "start_time": "14:00",
    "contact_firstname": "Mary",
    "contact_lastname": "Smith",
    "contact_phone": "+37255123456",
    "contact_email": "mary.smith@example.com",
    "message_text": "Prefer lighter tone highlights."
  }'
HTTP 201 Created
{
  "status": "success",
  "data": {
    "booking_id": "3b29c1e0-71a2-47e9-a02b-118833fa0011",
    "status": "On Hold",
    "hold_expires_at": "2026-11-08T21:12:30+02:00",
    "hold_duration_seconds": 900,
    "message": "Time reserved for 15 minutes. Verification code sent to mary.smith@example.com."
  }
}
HTTP 409 Conflict (Rule: 1 active booking)
{
  "status": "error",
  "code": "ACTIVE_BOOKING_EXISTS",
  "message": "Client already has an active booking in category 'hair' on 2026-11-20 11:00."
}

10. Confirm Booking with Email Verification Code

POST /v1/agent/booking/confirm
Bearer Token

Confirms an On Hold reservation by validating the client's 6-digit email confirmation code. Protected against brute-force attacks (up to 5 invalid attempts before lock). If the salon requires a deposit, a secure payment URL is issued.

Parameters (JSON body)

FieldTypeSource & Description
booking_id requireduuidSourced from Step 6 POST /v1/agent/booking/hold response field data.booking_id.
verification_code requiredstring6-digit verification code sent to client's email (e.g. "481902").
Request (cURL)
curl -X POST https://api.bjuti.ee/v1/agent/booking/confirm \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "booking_id": "3b29c1e0-71a2-47e9-a02b-118833fa0011",
    "verification_code": "481902"
  }'
HTTP 200 OK
{
  "status": "success",
  "data": {
    "booking_id": "3b29c1e0-71a2-47e9-a02b-118833fa0011",
    "status": "Confirmed",
    "confirmed_at": "2026-11-08T20:58:12+02:00",
    "requires_deposit": false,
    "payment_url": null,
    "summary": {
      "salon": "Studio Elegance Downtown",
      "professional": "Kadri Kallas",
      "date": "2026-11-15",
      "start_time": "14:00",
      "total_price": 45.00
    }
  }
}
HTTP 400 Bad Request
{
  "status": "error",
  "code": "INVALID_VERIFICATION_CODE",
  "message": "Invalid code. 3 attempts remaining."
}

11. Release Hold Slot Early

POST /v1/agent/booking/release-hold
Bearer Token

If the client decides to cancel or choose a different time, the agent can release the hold immediately without waiting for the 15-minute expiration timer.

Parameters (JSON body)

FieldTypeSource & Description
booking_id requireduuidSourced from Step 6 UUID of the active on-hold reservation (data.booking_id).
Request (cURL)
curl -X POST https://api.bjuti.ee/v1/agent/booking/release-hold \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "booking_id": "3b29c1e0-71a2-47e9-a02b-118833fa0011"
  }'
HTTP 200 OK
{
  "status": "success",
  "message": "Booking hold released successfully. Slot is available again."
}

12. Waiting List API (Waitlist)

POST /v1/agent/booking/waitlist
Bearer Token

Adds client to the professional's waiting list when no suitable opening is available on requested dates. If the client is already queued, preferences are updated while preserving their position. When an opening occurs, slots are allocated via FIFO.

Parameters (JSON body)

FieldTypeSource & Description
location_id requireduuidSourced from Step 3 Salon location UUID (salons[].location_id).
professional_id requireduuidSourced from Step 3 or 4 Specialist UUID.
service_ids requiredarray[uuid]Sourced from Step 4 Array of treatment UUIDs from pricelist.
contact_firstname requiredstringClient first name.
contact_lastname optionalstringClient last name.
contact_phone requiredstringClient mobile phone number.
contact_email requiredstringClient email address.
message_text optionalstringClient notes on preferred dates or times.
Request (cURL)
curl -X POST https://api.bjuti.ee/v1/agent/booking/waitlist \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "location_id": "96fb29cf-a031-4ce8-b541-ef12998a1234",
    "professional_id": "8a1e47b2-3c10-482a-99cd-482b991a0021",
    "service_ids": ["c14d9b2e-7412-4aa3-8fa9-994b2a8901aa"],
    "contact_firstname": "Mary",
    "contact_lastname": "Smith",
    "contact_phone": "+37255123456",
    "contact_email": "mary.smith@example.com",
    "message_text": "Prefer weekday evenings after 17:00."
  }'
HTTP 201 Created
{
  "status": "success",
  "message": "Client successfully added to waiting list.",
  "data": {
    "waitlist_id": "18cf41a0-8910-4bc3-90b1-4478129031ef",
    "already_waiting": false,
    "status": "waiting",
    "title": "Women's Haircut & Blow-dry",
    "duration_minutes": 60,
    "created_at": "2026-11-08T21:25:00+02:00"
  }
}
HTTP 200 OK (Already on waitlist)
{
  "status": "success",
  "message": "Client is already on the waiting list; request details updated.",
  "data": {
    "waitlist_id": "18cf41a0-8910-4bc3-90b1-4478129031ef",
    "already_waiting": true,
    "status": "waiting",
    "title": "Women's Haircut & Blow-dry",
    "duration_minutes": 60
  }
}

13. Query Client Active Bookings

GET /v1/agent/booking/my-active
Bearer Token

Allows an agent to check a client's active upcoming reservations prior to initiating a new booking, avoiding duplicate appointments in the same category.

Query Parameters

ParameterTypeDescription
contact_email requiredstringClient email address
category optionalstringCategory code filter (e.g. hair)
Request (cURL)
curl -X GET "https://api.bjuti.ee/v1/agent/booking/my-active?contact_email=mary.smith@example.com" \
  -H "Authorization: Bearer <access_token>"
HTTP 200 OK
{
  "status": "success",
  "data": [
    {
      "booking_id": "77a102bb-8199-4c01-bf10-9941a8291001",
      "status": "Confirmed",
      "category": "hair",
      "service_name": "Women's Haircut",
      "salon_name": "Studio Elegance Downtown",
      "start_time": "2026-11-20T11:00:00+02:00"
    }
  ]
}

What is the Bjuti MCP Server?

The Model Context Protocol (MCP) is an open standard enabling AI models and autonomous assistants (e.g. Claude Desktop, Cursor, bespoke agents) to securely interface with real-world business tools. Bjuti provides a dedicated, native MCP daemon that empowers agents to search salons, compute available slots, and manage reservations directly without middleware code.

MCP Server URL: https://api.bjuti.ee/mcp.
The daemon supports both JSON-RPC 2.0 requests and Server-Sent Events (SSE) streaming connections.

MCP Client Configuration (Examples)

If you are integrating Claude Desktop, Cursor, or any MCP-compliant AI framework, add the Bjuti MCP endpoint to your client configuration:

Example: Claude Desktop (`claude_desktop_config.json`)

Claude Desktop Config
{
  "mcpServers": {
    "bjuti": {
      "url": "https://api.bjuti.ee/mcp",
      "headers": {
        "Authorization": "Bearer <YOUR_ACCESS_TOKEN>"
      }
    }
  }
}

Example: Direct JSON-RPC Request over HTTPS

cURL JSON-RPC Request
curl -X POST https://api.bjuti.ee/mcp \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "bjuti_find_salons",
      "arguments": {
        "lat": 59.4369,
        "lng": 24.7535,
        "radius_km": 15
      }
    }
  }'

Published Bjuti MCP Tools

The Bjuti MCP server exposes the following specialized tools to connected AI agents:

Tool Name Input Parameters Description
bjuti_agent_auth client_id, client_secret Authenticates the agent and returns a 15-minute Bearer access token.
bjuti_get_service_categories country (optional) Retrieves available beauty service categories (Hair, Nails, Facial, etc.).
bjuti_find_salons lat, lng, radius_km, query Searches salons by GPS coordinates or text query.
bjuti_get_salon_pricelist location_id Fetches the official pricelist of treatments and prices for a salon location.
bjuti_get_available_slots location_id, professional_id, service_ids, date Computes available appointment slots based on calendar schedules.
bjuti_hold_booking location_id, professional_id, service_ids, date, start_time, contact_firstname, contact_phone, contact_email Locks a booking slot for 15 minutes and dispatches a 6-digit verification code to the client email.
bjuti_confirm_booking booking_id, verification_code Confirms an On Hold booking using the 6-digit email verification code.
bjuti_release_hold booking_id Releases an active 15-minute hold immediately before expiration.
bjuti_join_waitlist location_id, professional_id, service_ids, contact_firstname, contact_phone, contact_email Places client on the waiting list when requested dates are unavailable.
bjuti_get_active_booking contact_email, category_id Checks for active upcoming reservations to prevent duplicate bookings.