{"openapi":"3.1.0","info":{"title":"Bjuti AI Agent & Booking API","version":"1.0.0","description":"Official REST API for AI Agents, autonomous assistants, mobile applications, and external partner integrations to query beauty services, salons, available schedule slots, reserve 15-minute booking holds, and confirm appointments on the Bjuti platform (bjuti.ee).\n\n### AI Agent Booking Workflow Lifecycle & Invocation Sequence\nTo complete an appointment booking successfully, autonomous AI agents MUST invoke endpoints in the following strictly chained order:\n\n1. **Step 1: Authenticate Agent** (`POST /v1/agent/auth/token`)\n   - Input: `client_id`, `client_secret`\n   - Sourced output: `access_token` (Bearer token valid strictly for 15 minutes / 900 seconds) and `refresh_token`.\n   - Next: Include `Authorization: Bearer <access_token>` in all subsequent requests.\n\n2. **Step 2 (Optional): Discover Service Categories** (`GET /v1/agent/booking/service-groups`)\n   - Input: `country` (default: 'EE')\n   - Sourced output: `category_id`, `name` (Hair, Nails, Brows/Lashes, Skin, Massage, etc.).\n\n3. **Step 3: Search Salons & Specialists** (`GET /v1/agent/booking/nearby-salons` or `/search-salons`)\n   - Input: GPS coordinates (`lat`, `lng`, `radius_km`) OR text queries (`q`, `salon_name`, `specialist_name`).\n   - Sourced output:\n     - `location_id` (UUID of chosen salon location) -> Passed to Steps 4, 5, 6, and Waitlist\n     - `professionals[].professional_id` (UUID of chosen specialist) -> Passed to Steps 5, 6, and Waitlist\n\n4. **Step 4: Fetch Salon Pricelist & Services** (`GET /v1/agent/booking/pricelist`)\n   - Input: `location_id` (Sourced from Step 3: `salons[].location_id`)\n   - Sourced output:\n     - `pricelist[].service_id` (UUIDs of primary treatment and optional addons/subservices) -> Passed as `service_ids` to Steps 5, 6, and Waitlist\n     - `pricelist[].specialists[].professional_id` (Alternative specialist confirmation)\n\n5. **Step 5: Query Open Availability Slots** (`GET /v1/agent/booking/availability-slots`)\n   - Input:\n     - `location_id` (Sourced from Step 3: `salons[].location_id`)\n     - `professional_id` (Sourced from Step 3 or 4: `professional_id`)\n     - `service_ids` (Sourced from Step 4: `pricelist[].service_id`, comma-separated)\n     - `date` (Target date in YYYY-MM-DD)\n   - Sourced output:\n     - 4-day matrix of open start times (`slots[date][]`) -> Select client's preferred `start_time` (e.g. \"14:30\")\n     - If completely booked: returns `first_available_date` and `waitlist_available: true` flag\n\n6. **Step 6: Lock Slot On-Hold for 15 Minutes** (`POST /v1/agent/booking/hold`)\n   - Input:\n     - `location_id` (Sourced from Step 3)\n     - `professional_id` (Sourced from Step 3 or 4)\n     - `service_ids` (Sourced from Step 4)\n     - `date` (Sourced from Step 5)\n     - `start_time` (Sourced from Step 5: `slots[date][]`)\n     - Client contact info: `contact_firstname`, `contact_lastname`, `contact_phone`, `contact_email`, `message_text`\n   - Sourced output:\n     - `booking_id` (UUID of the held reservation) -> Passed to Step 7 (Confirm) or Release-Hold\n     - System sends a 6-digit verification code to `contact_email`.\n\n7. **Step 7: Confirm Booking with Verification Code** (`POST /v1/agent/booking/confirm`)\n   - Input:\n     - `booking_id` (Sourced from Step 6: `booking_id`)\n     - `verification_code` (6-digit code received by client via email)\n   - Sourced output: Booking transitions to `confirmed` (or `waiting_payment` if deposit required).\n\n### Branching / Fallback Actions:\n- **Cancel / Release Hold** (`POST /v1/agent/booking/release-hold`): Releases lock if user declines, requires `booking_id` from Step 6.\n- **Join Waitlist** (`POST /v1/agent/booking/waitlist`): If Step 5 yielded no available slots, requires `location_id`, `professional_id`, `service_ids`.","contact":{"name":"Bjuti Developer Support","email":"support@bjuti.ee","url":"https://bjuti.ee"}},"servers":[{"url":"https://api.bjuti.ee","description":"Production API Server"}],"components":{"securitySchemes":{"AgentBearerAuth":{"type":"http","scheme":"bearer","description":"Short-lived Bearer access token issued by Step 1: POST /v1/agent/auth/token. Valid strictly for 15 minutes (900 seconds). AI Agents must rotate using refresh_token."}},"schemas":{"AuthTokenRequest":{"type":"object","required":["client_id","client_secret"],"properties":{"client_id":{"type":"string","example":"ag_live_9b4c81...","description":"Agent client identifier issued in Bjuti partner portal"},"client_secret":{"type":"string","example":"sec_f20a91...","description":"Agent secret key issued in Bjuti partner portal"}}},"AuthTokenResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"data":{"type":"object","properties":{"access_token":{"type":"string","description":"Bearer token to pass in Authorization header for all subsequent workflow steps"},"token_type":{"type":"string","example":"Bearer"},"expires_in":{"type":"integer","example":900,"description":"Exact TTL in seconds (15 minutes)"},"expires_at":{"type":"string","format":"date-time"},"refresh_token":{"type":"string","description":"Longer-lived token to rotate access_token via /v1/agent/auth/refresh"}}}}},"BookingHoldRequest":{"type":"object","required":["location_id","professional_id","service_ids","date","start_time","contact_firstname","contact_phone","contact_email"],"properties":{"location_id":{"type":"string","format":"uuid","description":"[Parameter Chain: Step 3 /nearby-salons or /search-salons -> salons[].location_id] Unique UUID of the salon location"},"professional_id":{"type":"string","format":"uuid","description":"[Parameter Chain: Step 3 (salons[].professionals[].professional_id) or Step 4 (pricelist[].specialists[].professional_id)] Specialist UUID"},"service_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"[Parameter Chain: Step 4 /pricelist -> pricelist[].service_id] Array of service UUIDs including primary service and optional addons"},"date":{"type":"string","format":"date","example":"2026-11-15","description":"[Parameter Chain: Step 5 /availability-slots] Chosen appointment date in YYYY-MM-DD"},"start_time":{"type":"string","example":"14:30","description":"[Parameter Chain: Step 5 /availability-slots -> slots[date][]] Selected appointment start time string"},"contact_firstname":{"type":"string","example":"Mari","description":"Client first name"},"contact_lastname":{"type":"string","example":"Tamm","description":"Client last name"},"contact_phone":{"type":"string","example":"+37255123456","description":"Client mobile phone with international country code"},"contact_email":{"type":"string","format":"email","example":"mari.tamm@example.com","description":"Client email address where 6-digit confirmation code will be dispatched"},"message_text":{"type":"string","example":"Soovin juukseotsi lõigata ja heledamat tooni.","description":"Optional client note or wishes for the specialist"},"result_photo":{"type":"string","description":"Optional ImageKit URL or reference photo of desired result"}}},"BookingConfirmRequest":{"type":"object","required":["booking_id","verification_code"],"properties":{"booking_id":{"type":"string","format":"uuid","description":"[Parameter Chain: Step 6 /hold -> response data.booking_id] UUID of the active on-hold booking reservation"},"verification_code":{"type":"string","example":"481902","description":"6-digit verification code received by the client at contact_email"}}}}},"paths":{"/v1/agent/auth/token":{"post":{"summary":"Step 1 of 7: Authenticate AI Agent","description":"[WORKFLOW STEP 1: AUTHENTICATION]\nAuthenticates an AI Agent with client credentials and issues a short-lived access token (strictly 15 minutes / 900 seconds) and a refresh token. Rate limited to 10 attempts per minute.\n\n**Output passed to next steps:** `access_token` must be sent as `Authorization: Bearer <access_token>` in all subsequent requests.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenRequest"}}}},"responses":{"200":{"description":"Authenticated successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}},"401":{"description":"Invalid client_id or client_secret"},"429":{"description":"Rate limit exceeded"}}}},"/v1/agent/auth/refresh":{"post":{"summary":"Token Maintenance: Refresh Access Token","description":"Rotates credentials and issues a fresh 15-minute access token using a valid refresh token. AI Agents must rotate before the 900-second TTL expires.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["refresh_token"],"properties":{"refresh_token":{"type":"string","description":"[Parameter Chain: Step 1 /token -> response data.refresh_token] Active refresh token"}}}}}},"responses":{"200":{"description":"Token refreshed successfully"},"401":{"description":"Invalid or expired refresh token"}}}},"/v1/agent/booking/service-groups":{"get":{"summary":"Step 2 of 7 (Optional): List Service Categories","description":"[WORKFLOW STEP 2: CATEGORY DISCOVERY]\nFetches available beauty service categories (Hair, Nails, Brows/Lashes, Skin, Massage, etc.) to guide user intentions.\n\n**Output passed forward:** `category_id` can be used to filter searches and check active 1-booking-per-category limits.","security":[{"AgentBearerAuth":[]}],"parameters":[{"name":"country","in":"query","description":"ISO 3166-1 alpha-2 country code (default 'EE')","schema":{"type":"string","default":"EE"}}],"responses":{"200":{"description":"Categories list"}}}},"/v1/agent/booking/nearby-salons":{"get":{"summary":"Step 3 of 7 (Option A): Find Nearby Salons by Coordinates","description":"[WORKFLOW STEP 3A: SALON LOCATION DISCOVERY]\nFinds partner salons around GPS coordinates ordered by distance.\n\n**Output passed to next steps:**\n- `salons[].location_id` -> Pass to Step 4 (`/pricelist`), Step 5 (`/availability-slots`), and Step 6 (`/hold`).\n- `salons[].professionals[].professional_id` -> Pass to Step 5 (`/availability-slots`) and Step 6 (`/hold`).","security":[{"AgentBearerAuth":[]}],"parameters":[{"name":"lat","in":"query","required":true,"description":"Client latitude","schema":{"type":"number"}},{"name":"lng","in":"query","required":true,"description":"Client longitude","schema":{"type":"number"}},{"name":"radius_km","in":"query","description":"Search radius in kilometers (default 25)","schema":{"type":"number","default":25}}],"responses":{"200":{"description":"List of nearby salons with location_id, address, distance, and specialists"}}}},"/v1/agent/booking/search-salons":{"get":{"summary":"Step 3 of 7 (Option B): Search Salons & Specialists by Name","description":"[WORKFLOW STEP 3B: TEXT SEARCH FOR SALONS / SPECIALISTS]\nSearches salons by name, address, or specialist name, phone number, and email.\n\n**Output passed to next steps:**\n- `salons[].location_id` -> Pass to Step 4 (`/pricelist`), Step 5 (`/availability-slots`), and Step 6 (`/hold`).\n- `salons[].professionals[].professional_id` -> Pass to Step 5 (`/availability-slots`) and Step 6 (`/hold`).","security":[{"AgentBearerAuth":[]}],"parameters":[{"name":"q","in":"query","description":"Free text query (salon name, address, specialist name)","schema":{"type":"string"}},{"name":"salon_name","in":"query","description":"Filter by salon establishment name","schema":{"type":"string"}},{"name":"specialist_name","in":"query","description":"Filter by specific professional / master name","schema":{"type":"string"}}],"responses":{"200":{"description":"Matching salons and specialists with location_id and professional_id"}}}},"/v1/agent/booking/pricelist":{"get":{"summary":"Step 4 of 7: Get Salon Services Pricelist","description":"[WORKFLOW STEP 4: SERVICE SELECTION & PRICELIST]\nReturns all available treatments, durations, prices, categories, and connected add-ons (subservices) for a specific salon location.\n\n**Output passed to next steps:**\n- `pricelist[].service_id` -> Pass selected primary service and optional addons to Step 5 (`/availability-slots`) and Step 6 (`/hold`).","security":[{"AgentBearerAuth":[]}],"parameters":[{"name":"location_id","in":"query","required":true,"description":"[Parameter Chain: Step 3 /nearby-salons or /search-salons -> salons[].location_id] Salon location UUID","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Pricelist details with service_id, name, duration_minutes, price, and addons"}}}},"/v1/agent/booking/availability-slots":{"get":{"summary":"Step 5 of 7: Get Bookable Time Slots (4-day Window & First Available)","description":"[WORKFLOW STEP 5: AVAILABILITY & TIME SLOT SELECTION]\nComputes open, unblocked start times for a specialist and selected service(s) across a 4-day window (date to date + 3 days). If the requested window is completely booked, discovers the earliest upcoming available date (up to 45 days ahead).\n\n**Output passed to next steps:**\n- Select target `date` (YYYY-MM-DD) and a specific slot from `slots[date][]` as `start_time` (e.g. \"14:30\") -> Pass to Step 6 (`/hold`).\n- If no slots: `waitlist_available: true` allows triggering `/waitlist`.","security":[{"AgentBearerAuth":[]}],"parameters":[{"name":"location_id","in":"query","required":true,"description":"[Parameter Chain: Step 3 /nearby-salons -> salons[].location_id] Salon location UUID","schema":{"type":"string","format":"uuid"}},{"name":"professional_id","in":"query","required":true,"description":"[Parameter Chain: Step 3 /search-salons or Step 4 /pricelist -> specialists[].professional_id] Specialist UUID","schema":{"type":"string","format":"uuid"}},{"name":"service_ids","in":"query","required":true,"description":"[Parameter Chain: Step 4 /pricelist -> pricelist[].service_id] Comma-separated service UUIDs (including addons)","schema":{"type":"string"}},{"name":"date","in":"query","required":true,"description":"Start date for 4-day availability calculation window in YYYY-MM-DD","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"4-day slot map, first available date, and waitlist availability flag"}}}},"/v1/agent/booking/hold":{"post":{"summary":"Step 6 of 7: Hold Booking Slot (15 Minutes)","description":"[WORKFLOW STEP 6: TEMPORARY HOLD & CODE DISPATCH]\nAtomically locks the time slot in 'On Hold' status for 15 minutes. Validates that the client has no existing active booking in the same category. Generates and dispatches a 6-digit confirmation code to the client's email.\n\n**Output passed to next steps:**\n- `booking_id` -> Pass to Step 7 (`/confirm`) or to `/release-hold`.\n- Ask client for the 6-digit verification code sent to `contact_email`.","security":[{"AgentBearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingHoldRequest"}}}},"responses":{"200":{"description":"Slot held on hold for 15 minutes (returns booking_id), verification code sent to client email"},"409":{"description":"Slot already taken"},"422":{"description":"Active booking already exists in this category or missing required fields"}}}},"/v1/agent/booking/confirm":{"post":{"summary":"Step 7 of 7: Confirm Booking with Verification Code","description":"[WORKFLOW STEP 7: FINAL CONFIRMATION]\nSubmits the 6-digit email confirmation code. If code is valid, transitions the hold into Confirmed (or Waiting payment if salon requires deposit). Protected by 5-attempt limit.\n\n**Terminal step:** Successfully finalizes the booking in salon calendar.","security":[{"AgentBearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingConfirmRequest"}}}},"responses":{"200":{"description":"Booking successfully confirmed in salon calendar"},"410":{"description":"Hold has expired"},"422":{"description":"Invalid code or max attempts exceeded"}}}},"/v1/agent/booking/release-hold":{"post":{"summary":"Workflow Fallback: Release Booking Hold","description":"Immediately cancels and releases an active 'On Hold' booking so another client or agent can book it. Use if the client decides to cancel or change minds before entering the verification code.","security":[{"AgentBearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["booking_id"],"properties":{"booking_id":{"type":"string","format":"uuid","description":"[Parameter Chain: Step 6 /hold -> response data.booking_id] UUID of the active hold to release"}}}}}},"responses":{"200":{"description":"Hold released successfully"}}}},"/v1/agent/booking/waitlist":{"post":{"summary":"Workflow Fallback: Join Salon Professional Waitlist","description":"Adds client to the professional waiting list when Step 5 yielded no available booking slot. If the client is already waiting for this professional, updates the entry. As working slots open up, waitlist entries are automatically offered or booked FIFO.","security":[{"AgentBearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["location_id","professional_id","service_ids","contact_firstname","contact_phone","contact_email"],"properties":{"location_id":{"type":"string","format":"uuid","description":"[Parameter Chain: Step 3 /nearby-salons -> salons[].location_id] Salon location UUID"},"professional_id":{"type":"string","format":"uuid","description":"[Parameter Chain: Step 3 /search-salons or Step 4 /pricelist -> professional_id] Specialist UUID"},"service_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"[Parameter Chain: Step 4 /pricelist -> pricelist[].service_id] Array of desired service UUIDs"},"contact_firstname":{"type":"string","description":"Client first name"},"contact_lastname":{"type":"string","description":"Client last name"},"contact_phone":{"type":"string","description":"Client mobile phone"},"contact_email":{"type":"string","format":"email","description":"Client email"},"message_text":{"type":"string","description":"Optional notes on preferred dates/times"}}}}}},"responses":{"201":{"description":"Client joined waitlist successfully"},"200":{"description":"Waitlist entry already exists and was updated"},"422":{"description":"Validation error or missing fields"}}}},"/v1/agent/booking/my-active":{"get":{"summary":"Workflow Helper: Get Client Active Bookings","description":"Returns client active appointments filtered by email/phone and category. Useful to verify existing appointments before initiating a new hold (to prevent 1-booking-per-category violation).","security":[{"AgentBearerAuth":[]}],"parameters":[{"name":"email","in":"query","description":"Client email address","schema":{"type":"string"}},{"name":"phone","in":"query","description":"Client mobile phone number","schema":{"type":"string"}},{"name":"category","in":"query","description":"Service category ID or name (Hair, Nails, etc.)","schema":{"type":"string"}}],"responses":{"200":{"description":"List of active appointments"}}}}}}