{
  "openapi": "3.1.0",
  "info": {
    "title": "Recursive AI — Meeting Booking API",
    "version": "1.0.0",
    "summary": "Book, view, reschedule and cancel a meeting with Recursive AI.",
    "description": "Public JSON API behind recursiveai.org/book. Agents and the web page use the same endpoints.\n\nFlow: `get-options` → `get-availability` → `create-booking` (returns a secret manage `token`) → `get-booking` / `reschedule-booking` / `cancel-booking` / `get-booking-ics` with that token.\n\nAll instants are RFC 3339. `start`/`end` are UTC; `startLocal`/`endLocal` are in the requested IANA `timeZone`. The server is authoritative: a booking is accepted only if `start` is exactly one of the currently offered slot starts.\n\nWhen the calendar backend is unavailable every booking endpoint answers 503 with `code: booking_unavailable` — nothing is booked.",
    "contact": { "email": "partners@recursiveai.org" }
  },
  "servers": [{ "url": "https://recursiveai.org/book/api" }],
  "paths": {
    "/get-options": {
      "get": {
        "operationId": "getOptions",
        "summary": "Booking status, durations, topics, limits",
        "responses": {
          "200": { "description": "Options", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Options" } } } },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/get-availability": {
      "get": {
        "operationId": "getAvailability",
        "summary": "Offered slots for a duration and date range",
        "description": "Also accepts POST with the same fields as a JSON body. Every day in the range is listed, with an empty `slots` array when nothing is free.",
        "parameters": [
          { "name": "duration", "in": "query", "required": true, "schema": { "type": "integer", "examples": [30] }, "description": "Minutes; one of `durations` from get-options." },
          { "name": "timeZone", "in": "query", "required": true, "schema": { "type": "string", "examples": ["Europe/Berlin"] }, "description": "Visitor IANA time zone. Days and startLocal/endLocal use it." },
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "First date in timeZone. Default: today." },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Last date inclusive. Default: from + 13 days. Max span 62 days." }
        ],
        "responses": {
          "200": { "description": "Slots", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Availability" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/create-booking": {
      "post": {
        "operationId": "createBooking",
        "summary": "Book a meeting",
        "description": "Creates a calendar event with a Google Meet link and emails a confirmation to `email`. Store the returned `token`: it is the only way to view, reschedule or cancel. Rate limited per IP and per email.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateBookingRequest" } } } },
        "responses": {
          "201": { "description": "Booked", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateBookingResponse" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "405": { "$ref": "#/components/responses/Error" },
          "409": { "description": "`slot_unavailable` (taken or not offered) or `too_many_active_bookings`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "`rate_limited`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/get-booking": {
      "get": {
        "operationId": "getBooking",
        "summary": "View a booking by manage token",
        "description": "Also accepts POST {\"token\": \"…\"} (preferred — keeps the token out of URLs).",
        "parameters": [
          { "name": "token", "in": "query", "required": true, "schema": { "$ref": "#/components/schemas/Token" } },
          { "name": "timeZone", "in": "query", "schema": { "type": "string" }, "description": "Optional display zone. Default: the zone used when booking." }
        ],
        "responses": {
          "200": { "description": "Booking", "content": { "application/json": { "schema": { "type": "object", "required": ["booking"], "properties": { "booking": { "$ref": "#/components/schemas/Booking" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/reschedule-booking": {
      "post": {
        "operationId": "rescheduleBooking",
        "summary": "Move a booking to another offered slot",
        "description": "Only confirmed, future bookings. The Meet link stays the same. A reschedule email is sent.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RescheduleRequest" } } } },
        "responses": {
          "200": { "description": "Rescheduled", "content": { "application/json": { "schema": { "type": "object", "required": ["booking"], "properties": { "booking": { "$ref": "#/components/schemas/Booking" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "`slot_unavailable`, `booking_cancelled`, `booking_pending`, `too_late` or `reschedule_limit`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/cancel-booking": {
      "post": {
        "operationId": "cancelBooking",
        "summary": "Cancel a booking",
        "description": "Idempotent: cancelling a cancelled booking returns 200 with alreadyCancelled=true. A cancellation email is sent.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelRequest" } } } },
        "responses": {
          "200": { "description": "Cancelled", "content": { "application/json": { "schema": { "type": "object", "required": ["booking", "alreadyCancelled"], "properties": { "booking": { "$ref": "#/components/schemas/Booking" }, "alreadyCancelled": { "type": "boolean" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "`too_late` or `booking_pending`", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "$ref": "#/components/responses/Unavailable" }
        }
      }
    },
    "/get-booking-ics": {
      "get": {
        "operationId": "getBookingIcs",
        "summary": "Download the booking as an .ics calendar file",
        "description": "Stable UID; SEQUENCE increases on reschedule. Cancelled bookings return METHOD:CANCEL so importing removes the event.",
        "parameters": [ { "name": "token", "in": "query", "required": true, "schema": { "$ref": "#/components/schemas/Token" } } ],
        "responses": {
          "200": { "description": "iCalendar file", "content": { "text/calendar": { "schema": { "type": "string" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Token": { "type": "string", "pattern": "^[A-Za-z0-9_-]{43}$", "description": "Secret manage token (256-bit, URL-safe). Treat like a password." },
      "Error": {
        "type": "object",
        "required": ["error", "code"],
        "properties": {
          "error": { "type": "string", "description": "Human-readable message, safe to show." },
          "code": { "type": "string", "enum": ["invalid_request", "method_not_allowed", "slot_unavailable", "too_many_active_bookings", "rate_limited", "not_found", "booking_cancelled", "booking_pending", "too_late", "reschedule_limit", "conflict", "booking_unavailable"] }
        }
      },
      "Topic": { "type": "object", "required": ["id", "label"], "properties": { "id": { "type": "string" }, "label": { "type": "string" } } },
      "Options": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["available", "unavailable"] },
          "message": { "type": "string", "description": "Present when unavailable." },
          "hostTimeZone": { "type": "string", "examples": ["America/Chicago"] },
          "durations": { "type": "array", "items": { "type": "integer" }, "examples": [[30, 60]] },
          "topics": { "type": "array", "items": { "$ref": "#/components/schemas/Topic" } },
          "minimumNoticeMinutes": { "type": "integer" },
          "horizonDays": { "type": "integer" },
          "slotGranularityMinutes": { "type": "integer" },
          "maxReschedulesPerBooking": { "type": "integer" },
          "fields": { "type": "object", "description": "Per-field {required, maxLength} for name, email, company, note." },
          "docs": { "type": "object", "properties": { "openapi": { "type": "string" }, "markdown": { "type": "string" } } },
          "testMode": { "type": "boolean", "description": "Only present on a non-production test setup." }
        }
      },
      "Slot": {
        "type": "object",
        "required": ["start", "end", "startLocal", "endLocal", "label"],
        "properties": {
          "start": { "type": "string", "format": "date-time", "description": "UTC. Pass this to create-booking / reschedule-booking." },
          "end": { "type": "string", "format": "date-time" },
          "startLocal": { "type": "string", "format": "date-time", "description": "Same instant with the visitor zone's offset." },
          "endLocal": { "type": "string", "format": "date-time" },
          "label": { "type": "string", "description": "HH:MM in the visitor zone.", "examples": ["16:30"] }
        }
      },
      "Availability": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "const": "available" },
          "timeZone": { "type": "string" },
          "hostTimeZone": { "type": "string" },
          "duration": { "type": "integer" },
          "from": { "type": "string", "format": "date" },
          "to": { "type": "string", "format": "date" },
          "slotCount": { "type": "integer" },
          "generatedAt": { "type": "string", "format": "date-time" },
          "days": { "type": "array", "items": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "weekday": { "type": "string" }, "slots": { "type": "array", "items": { "$ref": "#/components/schemas/Slot" } } } } }
        }
      },
      "CreateBookingRequest": {
        "type": "object",
        "required": ["start", "duration", "topic", "name", "email", "note", "timeZone"],
        "properties": {
          "start": { "type": "string", "format": "date-time", "description": "Exactly a slot `start` from get-availability." },
          "duration": { "type": "integer", "description": "Minutes (30 or 60 by default)." },
          "topic": { "type": "string", "description": "Topic id, e.g. project, fractional-cto, something-else." },
          "name": { "type": "string", "maxLength": 100 },
          "email": { "type": "string", "format": "email", "maxLength": 254 },
          "company": { "type": "string", "maxLength": 120 },
          "note": { "type": "string", "maxLength": 2000, "description": "What you'd like to discuss." },
          "timeZone": { "type": "string", "description": "Visitor IANA zone, used in emails." },
          "website": { "type": "string", "description": "Honeypot. Leave empty / omit; humans never see it." }
        }
      },
      "CreateBookingResponse": {
        "type": "object",
        "required": ["booking", "token", "manageUrl"],
        "properties": {
          "booking": { "$ref": "#/components/schemas/Booking" },
          "token": { "$ref": "#/components/schemas/Token" },
          "manageUrl": { "type": "string", "format": "uri" }
        }
      },
      "RescheduleRequest": {
        "type": "object",
        "required": ["token", "start"],
        "properties": {
          "token": { "$ref": "#/components/schemas/Token" },
          "start": { "type": "string", "format": "date-time" },
          "duration": { "type": "integer", "description": "Optional new duration." },
          "timeZone": { "type": "string", "description": "Optional new visitor zone." }
        }
      },
      "CancelRequest": {
        "type": "object",
        "required": ["token"],
        "properties": { "token": { "$ref": "#/components/schemas/Token" }, "reason": { "type": "string", "maxLength": 1000 } }
      },
      "Booking": {
        "type": "object",
        "properties": {
          "reference": { "type": "string", "examples": ["RB-7K3QX9"], "description": "Short human reference (not a secret)." },
          "status": { "type": "string", "enum": ["confirmed", "cancelled", "pending"] },
          "topic": { "$ref": "#/components/schemas/Topic" },
          "durationMinutes": { "type": "integer" },
          "start": { "type": "string", "format": "date-time" },
          "end": { "type": "string", "format": "date-time" },
          "timeZone": { "type": "string" },
          "startLocal": { "type": "string", "format": "date-time" },
          "endLocal": { "type": "string", "format": "date-time" },
          "hostTimeZone": { "type": "string" },
          "meetUrl": { "type": "string", "format": "uri", "description": "Present while confirmed." },
          "name": { "type": "string" },
          "email": { "type": "string" },
          "company": { "type": "string" },
          "note": { "type": "string" },
          "rescheduleCount": { "type": "integer" },
          "canReschedule": { "type": "boolean" },
          "canCancel": { "type": "boolean" },
          "links": { "type": "object", "properties": { "manage": { "type": "string" }, "ics": { "type": "string" }, "googleCalendar": { "type": "string", "description": "Add-to-Google-Calendar link for the visitor's own calendar." } } },
          "testMode": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" }
        }
      }
    },
    "responses": {
      "BadRequest": { "description": "`invalid_request` — the message names the field.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "`not_found` — unknown or malformed token.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unavailable": { "description": "`booking_unavailable` — calendar/config not available. Nothing was booked or changed. Retry later or email partners@recursiveai.org.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Error": { "description": "Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    }
  }
}
