{
  "openapi": "3.1.0",
  "info": {
    "title": "OhMyDesk Booking API",
    "version": "1.0.0",
    "description": "Programmatic access to a coworking space's booking domain — availability plus desk, meeting-room, whole-day and private-office writes, and listing/cancelling your bookings with an API key, with more coming. Served from ohmydesk.app; the Supabase backend is never exposed.\n\nTwo auth tiers:\n- **Public (no key):** availability reads and desk-booking creation for a space, by its public slug. Safe to call from a browser or a public website.\n- **Authenticated (per-org API key):** your own organization's data — availability, and listing/cancelling bookings. Send the key as `Authorization: Bearer omd_live_…` (or `x-api-key`). Create keys in Settings → Integrations → API.\n\nEvery response uses the envelope `{ \"data\": …, \"error\": …, \"meta\": … }`.",
    "contact": { "name": "OhMyDesk", "email": "hello@ohmydesk.app", "url": "https://ohmydesk.app/developers" }
  },
  "servers": [
    { "url": "https://ohmydesk.app/api/v1", "description": "Production" }
  ],
  "tags": [
    { "name": "Public", "description": "No API key — scoped to a space's public slug." },
    { "name": "Account", "description": "Requires a per-org API key." }
  ],
  "paths": {
    "/public/{slug}/availability": {
      "get": {
        "tags": ["Public"],
        "summary": "Get a space's public availability",
        "description": "Desks, meeting rooms, private offices and the currently booked slots for a space. This is the same read the public booking page and the embeddable widget use.",
        "security": [],
        "parameters": [
          { "$ref": "#/components/parameters/Slug" }
        ],
        "responses": {
          "200": {
            "description": "Availability snapshot.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AvailabilityEnvelope" }
              }
            }
          },
          "404": {
            "description": "The space does not exist or public booking is disabled.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
          }
        }
      }
    },
    "/public/{slug}/bookings": {
      "post": {
        "tags": ["Public"],
        "summary": "Create a desk booking",
        "description": "Books a free desk for each requested date. A desk is auto-assigned per date from the space's live availability — you do not choose a specific desk. The space's operator (and the visitor, if an email is given) is notified.\n\nSend an `Idempotency-Key` header to make the call safe to retry: a repeated key replays the original response instead of creating a second booking.",
        "security": [],
        "parameters": [
          { "$ref": "#/components/parameters/Slug" },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "A unique key you generate per booking attempt. Retrying with the same key returns the original result and never double-books.",
            "schema": { "type": "string", "example": "a1b2c3d4-booking-2026-07-20" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/BookingRequest" },
              "example": {
                "dates": ["2026-07-20", "2026-07-21"],
                "visitorName": "Ada Lovelace",
                "visitorEmail": "ada@example.com",
                "visitorPhone": "+359 888 123 456",
                "notes": "Prefers a quiet corner"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Booking created. `data.bookings` lists the assigned desk for each date.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BookingEnvelope" },
                "example": {
                  "data": { "bookings": [ { "date": "2026-07-20", "deskId": "room2-desk4", "deskLabel": "Desk 8" } ] },
                  "error": null,
                  "meta": { "slug": "your-space" }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request — missing `visitorName`, no dates, a malformed date, or a date outside the bookable window. `error.code` is one of `missing_visitor_name`, `no_dates`, `invalid_date`, `date_out_of_range`, `invalid_body`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
          },
          "404": {
            "description": "The space does not exist or public booking is disabled (`org_not_found_or_disabled`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
          },
          "409": {
            "description": "No desk is free on one of the requested dates (`no_desks_available`), or a booking with this `Idempotency-Key` is still processing (`processing`).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
          }
        }
      }
    },
    "/public/{slug}/meeting-room-bookings": {
      "post": {
        "tags": ["Public"],
        "summary": "Book a meeting room (hourly)",
        "description": "Books a meeting room for a time slot on one day. Times are minutes from midnight, aligned to the room's slotMinutes and within openMinute/closeMinute (from availability). visitorEmail is required. Company fields create a B2B draft invoice. Honors Idempotency-Key.",
        "security": [],
        "parameters": [
          { "$ref": "#/components/parameters/Slug" },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MeetingRoomBookingRequest" } } }
        },
        "responses": {
          "201": { "description": "Booking created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SingleBookingEnvelope" } } } },
          "400": { "description": "Validation error — one of `missing_visitor_name`, `missing_visitor_email`, `invalid_date`, `invalid_range`, `invalid_slot`, `below_minimum`, `above_maximum`, `outside_open_hours`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "404": { "description": "`org_not_found_or_disabled` or `room_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "The slot is already taken (`slot_taken`), or the key is still processing (`processing`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    },
    "/public/{slug}/whole-day-bookings": {
      "post": {
        "tags": ["Public"],
        "summary": "Book a room for whole days",
        "description": "Books a whole-day-enabled room across a date range (max 60 days). When whole-day rooms are free, returns 201 with a booking_group_id. When the space takes card payment online, returns 200 with a Stripe checkoutUrl instead (visitorEmail required); the booking is created after payment. An optional returnUrl controls the buyer's landing page. Honors Idempotency-Key.",
        "security": [],
        "parameters": [
          { "$ref": "#/components/parameters/Slug" },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WholeDayBookingRequest" } } }
        },
        "responses": {
          "200": { "description": "The space takes card payment online — `data.checkoutUrl` is a Stripe Checkout URL; redirect the buyer there. The booking is created after payment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckoutEnvelope" } } } },
          "201": { "description": "Booking created (free whole-day room).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SingleBookingEnvelope" } } } },
          "400": { "description": "Validation error — `missing_visitor_name`, `missing_visitor_email`, `invalid_date`, `invalid_range`, `invalid_return_url`, `past_date`, `range_too_long`, `whole_day_not_available`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "404": { "description": "`org_not_found_or_disabled` or `room_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "A day in the range is already taken (`slot_taken`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "502": { "description": "Could not start Stripe checkout (`checkout_failed`) — safe to retry.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "503": { "description": "Card payment is on but the space hasn't finished its Stripe setup (`payment_not_configured`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    },
    "/public/{slug}/office-requests": {
      "post": {
        "tags": ["Public"],
        "summary": "Request a private office",
        "description": "Registers interest in a private office. Normally creates a lead for the operator (202), not a confirmed booking; companyName is required. When the space takes card payment online for an available office, returns 200 with a Stripe checkoutUrl instead (email required; paying the first month reserves the office). An optional returnUrl controls the buyer's landing page. Honors Idempotency-Key.",
        "security": [],
        "parameters": [
          { "$ref": "#/components/parameters/Slug" },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OfficeRequest" } } }
        },
        "responses": {
          "200": { "description": "The space takes card payment online for this office — `data.checkoutUrl` is a Stripe Checkout URL; redirect the buyer there. The office agreement opens after payment.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckoutEnvelope" } } } },
          "202": {
            "description": "Request received (a lead was created).",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ReceivedEnvelope" }, "example": { "data": { "received": true }, "error": null, "meta": { "slug": "your-space" } } } }
          },
          "400": { "description": "`company_required`, `missing_visitor_email` (paid path), or `invalid_return_url`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "404": { "description": "`org_not_found_or_disabled` or `office_not_found`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "The office already has an active agreement (`office_taken`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "502": { "description": "Could not start Stripe checkout (`checkout_failed`) — safe to retry.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "503": { "description": "Card payment is on but the space hasn't finished its Stripe setup (`payment_not_configured`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    },
    "/": {
      "get": {
        "tags": ["Account"],
        "summary": "API + organization identity",
        "description": "Confirms your API key and returns the organization it belongs to. A useful smoke test.",
        "responses": {
          "200": {
            "description": "Identity of the key's organization.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/IdentityEnvelope" },
                "example": {
                  "data": { "name": "OhMyDesk Booking API", "version": "v1", "organization": { "id": "…", "name": "Your Space", "slug": "your-space", "currency": "EUR" } },
                  "error": null
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
          }
        }
      }
    },
    "/availability": {
      "get": {
        "tags": ["Account"],
        "summary": "Your own organization's availability",
        "description": "The same shape as the public availability read, resolved for the organization your API key belongs to (member-booking view).",
        "responses": {
          "200": {
            "description": "Availability snapshot.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AvailabilityEnvelope" } } }
          },
          "401": {
            "description": "Missing, invalid, or revoked API key.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } }
          }
        }
      }
    },
    "/bookings": {
      "get": {
        "tags": ["Account"],
        "summary": "List your bookings",
        "description": "Lists your organization's desk and meeting-room bookings in a date window. Frozen desk days and cancelled meeting-room bookings are excluded. Each row carries a `type` (`desk` or `room`) and the `id` used by the cancel endpoint. A multi-day desk booking appears as one row per day, each with its own id.",
        "parameters": [
          { "name": "from", "in": "query", "required": false, "description": "Start of the date window (YYYY-MM-DD). Matched on each booking's date. Defaults to today.", "schema": { "type": "string", "format": "date", "example": "2026-07-20" } },
          { "name": "to", "in": "query", "required": false, "description": "End of the date window (YYYY-MM-DD, inclusive). Defaults to today + 30 days. The span may not exceed 366 days.", "schema": { "type": "string", "format": "date", "example": "2026-08-20" } },
          { "name": "type", "in": "query", "required": false, "description": "Which bookings to return.", "schema": { "type": "string", "enum": ["all", "desk", "room"], "default": "all" } }
        ],
        "responses": {
          "200": { "description": "Bookings in the window, sorted by date.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingListEnvelope" } } } },
          "400": { "description": "`invalid_type`, `invalid_date`, `invalid_range` (`to` before `from`), or `range_too_long`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "401": { "description": "Missing, invalid, or revoked API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    },
    "/bookings/{id}/cancel": {
      "post": {
        "tags": ["Account"],
        "summary": "Cancel a booking",
        "description": "Cancels a booking your organization owns, identified by the `id` from the list endpoint. A numeric id cancels a desk booking (removed; if it was a flex day the member's flex allowance is refunded). A UUID cancels a meeting-room booking (marked cancelled); cancelling one day of a whole-day reservation cancels the whole group. An id that doesn't belong to your organization returns `booking_not_found` — it never affects another space's data. Honors `Idempotency-Key`. Smart-lock access codes are not yet revoked automatically on cancel.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "description": "A numeric desk-booking id or a meeting-room UUID, from the list endpoint.", "schema": { "type": "string", "example": "720307628" } },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "responses": {
          "200": { "description": "Cancelled. `data` reports what happened (see CancelEnvelope). Re-cancelling an already-cancelled room returns 200 with `already: true`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelEnvelope" } } } },
          "400": { "description": "The id is neither a meeting-room UUID nor a numeric desk-booking id (`invalid_id`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "401": { "description": "Missing, invalid, or revoked API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "404": { "description": "No booking with that id belongs to your organization (`booking_not_found`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "A cancel with this `Idempotency-Key` is still processing (`processing`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    },
    "/bookings/{id}": {
      "patch": {
        "tags": ["Account"],
        "summary": "Edit or reschedule a booking",
        "description": "Edits or reschedules a booking your organization owns — a meeting-room booking (the `id` is a room UUID) or a single-day desk booking (the `id` is a numeric desk-booking id), both from the list endpoint. Send only the fields to change; omitted fields are left as-is. Meeting room: reschedule via `date` and/or `startMinute`/`endMinute` (minutes from midnight) — validated against opening hours, slot size, and min/max length, then re-checked for conflicts; rescheduling without a `price` recomputes it from the room's hourly rate. Desk: reschedule via `date` and/or `deskId` — applied in place (the booking keeps its id) and re-checked for conflicts. Desk edits cover single-day visitor bookings only: a member-plan or multi-day desk booking returns `member_booking_edit_unsupported`, and a whole-day / multi-day meeting-room group returns `group_edit_unsupported`. An unknown `deskId` returns `desk_not_found`; an id that isn't yours returns `booking_not_found`. Honors `Idempotency-Key`.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "description": "A meeting-room booking UUID or a numeric desk-booking id, from the list endpoint.", "schema": { "type": "string" } },
          { "$ref": "#/components/parameters/IdempotencyKey" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingUpdateRequest" } } }
        },
        "responses": {
          "200": { "description": "Updated. `data` echoes the booking's new state.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateEnvelope" } } } },
          "400": { "description": "`no_changes` (no editable fields), `invalid_status`, `invalid_slot`, `outside_open_hours`, `below_minimum`, `above_maximum`, `invalid_range`, `invalid_date`, `past_date`, or `invalid_id`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "401": { "description": "Missing, invalid, or revoked API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "404": { "description": "No booking with that id belongs to your organization (`booking_not_found`), or an unknown `deskId` (`desk_not_found`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "409": { "description": "The new meeting-room time or the target desk + date conflicts with another booking (`slot_taken`), the booking is cancelled (`booking_cancelled`), it's a whole-day group (`group_edit_unsupported`), it's a member-plan / multi-day desk booking (`member_booking_edit_unsupported`), or the `Idempotency-Key` is still processing (`processing`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "omd_live_…",
        "description": "Your per-org API key. Create one in Settings → Integrations → API. May also be sent as the `x-api-key` header."
      }
    },
    "security": [ { "ApiKeyBearer": [] } ],
    "parameters": {
      "Slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "The space's public booking slug (the last path segment of its /book/<slug> page).",
        "schema": { "type": "string", "example": "your-space" }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "A unique key you generate per attempt. Retrying with the same key replays the original response and never creates a duplicate.",
        "schema": { "type": "string", "example": "a1b2c3d4-2026-07-20" }
      }
    },
    "schemas": {
      "BookingRequest": {
        "type": "object",
        "required": ["dates", "visitorName"],
        "properties": {
          "dates": {
            "type": "array",
            "items": { "type": "string", "format": "date", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" },
            "minItems": 1,
            "description": "One or more dates (YYYY-MM-DD), each within the space's bookable window.",
            "example": ["2026-07-20", "2026-07-21"]
          },
          "visitorName": { "type": "string", "description": "Who the booking is for. Required.", "example": "Ada Lovelace" },
          "visitorEmail": { "type": "string", "format": "email", "nullable": true, "description": "If given, the visitor receives a confirmation email.", "example": "ada@example.com" },
          "visitorPhone": { "type": "string", "nullable": true, "example": "+359 888 123 456" },
          "notes": { "type": "string", "nullable": true, "description": "Free-text note passed to the operator.", "example": "Prefers a quiet corner" }
        }
      },
      "MeetingRoomBookingRequest": {
        "type": "object",
        "required": ["meetingRoomId", "date", "startMinute", "endMinute", "visitorName", "visitorEmail"],
        "properties": {
          "meetingRoomId": { "type": "string", "format": "uuid", "description": "A meeting room id from availability." },
          "date": { "type": "string", "format": "date", "example": "2026-07-20" },
          "startMinute": { "type": "integer", "description": "Minutes from midnight (600 = 10:00). Aligned to the room's slotMinutes, within openMinute/closeMinute.", "example": 600 },
          "endMinute": { "type": "integer", "example": 660 },
          "visitorName": { "type": "string", "example": "Ada Lovelace" },
          "visitorEmail": { "type": "string", "format": "email", "example": "ada@example.com" },
          "visitorPhone": { "type": "string", "nullable": true },
          "notes": { "type": "string", "nullable": true },
          "companyName": { "type": "string", "nullable": true, "description": "Supply company fields for a B2B booking (creates a draft invoice)." },
          "companyAddress": { "type": "string", "nullable": true },
          "companyVatId": { "type": "string", "nullable": true }
        }
      },
      "WholeDayBookingRequest": {
        "type": "object",
        "required": ["meetingRoomId", "startDate", "endDate", "visitorName", "visitorEmail"],
        "properties": {
          "meetingRoomId": { "type": "string", "format": "uuid" },
          "startDate": { "type": "string", "format": "date", "example": "2026-07-20" },
          "endDate": { "type": "string", "format": "date", "description": "Inclusive; up to 60 days from startDate.", "example": "2026-07-22" },
          "visitorName": { "type": "string" },
          "visitorEmail": { "type": "string", "format": "email", "description": "Required when the space takes card payment online for whole-day rooms (Stripe needs it for the receipt)." },
          "visitorPhone": { "type": "string", "nullable": true },
          "notes": { "type": "string", "nullable": true },
          "companyName": { "type": "string", "nullable": true },
          "companyAddress": { "type": "string", "nullable": true },
          "companyVatId": { "type": "string", "nullable": true },
          "returnUrl": { "type": "string", "format": "uri", "nullable": true, "description": "Paid path only. Absolute http(s) URL the buyer returns to after Stripe checkout; OhMyDesk appends ?payment=success&session_id=… or ?payment=cancelled. Defaults to the space's hosted booking page.", "example": "https://your-site.com/booking/done" }
        }
      },
      "OfficeRequest": {
        "type": "object",
        "required": ["officeId", "companyName"],
        "properties": {
          "officeId": { "type": "string", "format": "uuid", "description": "A private office id from availability." },
          "companyName": { "type": "string", "example": "Difference Engine Ltd" },
          "contactName": { "type": "string", "nullable": true },
          "email": { "type": "string", "format": "email", "nullable": true, "description": "Required when the space takes card payment online for an available office." },
          "phone": { "type": "string", "nullable": true },
          "preferredStart": { "type": "string", "nullable": true, "description": "Free-text preferred start (a date, or 'Flexible')." },
          "message": { "type": "string", "nullable": true },
          "companyAddress": { "type": "string", "nullable": true },
          "companyVatId": { "type": "string", "nullable": true },
          "returnUrl": { "type": "string", "format": "uri", "nullable": true, "description": "Paid path only. Absolute http(s) URL the buyer returns to after Stripe checkout; OhMyDesk appends ?payment=success&session_id=… or ?payment=cancelled. Defaults to the space's hosted booking page.", "example": "https://your-site.com/booking/done" }
        }
      },
      "SingleBookingEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "booking": { "type": "object", "description": "The created booking. Hourly returns booking_id/share_token/price/currency/duration_minutes/start_time/end_time; whole-day returns booking_group_id/invoice_id/days/price/currency." }
            }
          },
          "error": { "type": "null" },
          "meta": { "type": "object", "properties": { "slug": { "type": "string" } } }
        }
      },
      "CheckoutEnvelope": {
        "type": "object",
        "description": "Returned when a paid resource needs card payment. Redirect the buyer to `data.checkoutUrl`; the booking is created by the Stripe webhook after payment.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "checkoutUrl": { "type": "string", "format": "uri", "description": "A Stripe Checkout URL — send the buyer here.", "example": "https://checkout.stripe.com/c/pay/cs_test_…" },
              "paymentRequired": { "type": "boolean", "enum": [true], "description": "Always true — signals the caller to redirect rather than treat the response as a confirmed booking." }
            }
          },
          "error": { "type": "null" },
          "meta": { "type": "object", "properties": { "slug": { "type": "string" } } }
        }
      },
      "ReceivedEnvelope": {
        "type": "object",
        "properties": {
          "data": { "type": "object", "properties": { "received": { "type": "boolean" } } },
          "error": { "type": "null" },
          "meta": { "type": "object", "properties": { "slug": { "type": "string" } } }
        }
      },
      "Assignment": {
        "type": "object",
        "properties": {
          "date": { "type": "string", "format": "date", "example": "2026-07-20" },
          "deskId": { "type": "string", "description": "The space's business key for the assigned desk.", "example": "room2-desk4" },
          "deskLabel": { "type": "string", "example": "Desk 8" }
        }
      },
      "BookingEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "bookings": { "type": "array", "items": { "$ref": "#/components/schemas/Assignment" } }
            }
          },
          "error": { "type": "null" },
          "meta": { "type": "object", "properties": { "slug": { "type": "string" } } }
        }
      },
      "AvailabilityEnvelope": {
        "type": "object",
        "description": "The `data` object contains `org`, `rooms[]` (each with `desks[]` carrying `deskId` + `label`), `meetingRooms[]`, `privateOffices[]`, and `bookedSlots[]` (`{ deskId, date }`).",
        "properties": {
          "data": { "type": "object" },
          "error": { "type": "null" },
          "meta": { "type": "object" }
        }
      },
      "IdentityEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "version": { "type": "string" },
              "organization": {
                "type": "object",
                "properties": {
                  "id": { "type": "string" },
                  "name": { "type": "string" },
                  "slug": { "type": "string" },
                  "currency": { "type": "string" }
                }
              }
            }
          },
          "error": { "type": "null" }
        }
      },
      "Booking": {
        "type": "object",
        "description": "A booking row from the list endpoint. `type: desk` rows carry deskId/startDate/endDate/isFlex/clientId; `type: room` rows carry meetingRoomId/startTime/endTime/bookingGroupId/isFullDay. `id` is what you pass to the cancel endpoint.",
        "properties": {
          "type": { "type": "string", "enum": ["desk", "room"] },
          "id": { "type": "string", "description": "Numeric string for desks, UUID for meeting rooms.", "example": "720307628" },
          "date": { "type": "string", "format": "date", "example": "2026-07-20" },
          "status": { "type": "string", "example": "assigned" },
          "personName": { "type": "string", "nullable": true },
          "title": { "type": "string", "nullable": true },
          "price": { "type": "number", "nullable": true },
          "currency": { "type": "string", "nullable": true },
          "deskId": { "type": "string", "nullable": true, "description": "Desk rows only." },
          "startDate": { "type": "string", "nullable": true, "description": "Desk rows only." },
          "endDate": { "type": "string", "nullable": true, "description": "Desk rows only." },
          "isFlex": { "type": "boolean", "nullable": true, "description": "Desk rows only." },
          "clientId": { "type": "string", "nullable": true, "description": "Desk rows only — the member id, when the booking belongs to one." },
          "meetingRoomId": { "type": "string", "nullable": true, "description": "Room rows only." },
          "startTime": { "type": "string", "format": "date-time", "nullable": true, "description": "Room rows only." },
          "endTime": { "type": "string", "format": "date-time", "nullable": true, "description": "Room rows only." },
          "bookingGroupId": { "type": "string", "nullable": true, "description": "Room rows only — set when the row is part of a whole-day/multi-day group." },
          "isFullDay": { "type": "boolean", "nullable": true, "description": "Room rows only." },
          "visitorName": { "type": "string", "nullable": true },
          "visitorEmail": { "type": "string", "nullable": true },
          "visitorPhone": { "type": "string", "nullable": true }
        }
      },
      "BookingListEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "bookings": { "type": "array", "items": { "$ref": "#/components/schemas/Booking" } },
              "range": { "type": "object", "properties": { "from": { "type": "string", "format": "date" }, "to": { "type": "string", "format": "date" } } }
            }
          },
          "error": { "type": "null" },
          "meta": { "type": "object", "properties": { "organization_id": { "type": "string" } } }
        }
      },
      "CancelEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "description": "`cancelled` is always true. Desk cancels add type/id/deskId/date/flexRestored. Room cancels add type/id/rowsCancelled, plus groupId for a whole-day group, or already:true when it was already cancelled.",
            "properties": {
              "cancelled": { "type": "boolean", "enum": [true] },
              "type": { "type": "string", "enum": ["desk", "room"] },
              "id": { "type": "string" },
              "deskId": { "type": "string", "description": "Desk cancels." },
              "date": { "type": "string", "format": "date", "description": "Desk cancels." },
              "flexRestored": { "type": "boolean", "description": "Desk cancels — true when a flex day was refunded to the member." },
              "groupId": { "type": "string", "description": "Room cancels — present when a whole-day group was cancelled." },
              "rowsCancelled": { "type": "integer", "description": "Room cancels — how many rows were marked cancelled." },
              "already": { "type": "boolean", "description": "Room cancels — true when the booking was already cancelled." }
            }
          },
          "error": { "type": "null" },
          "meta": { "type": "object", "properties": { "organization_id": { "type": "string" } } }
        }
      },
      "BookingUpdateRequest": {
        "type": "object",
        "minProperties": 1,
        "description": "Partial edit of a meeting-room or desk booking. Send only the fields to change; at least one is required. Reschedule fields trigger validation + a conflict re-check. Meeting-room fields: date, startMinute, endMinute, price, companyName/Address/VatId. Desk fields: date, deskId, visitorName, visitorPhone. personName/title/notes/status apply to both.",
        "properties": {
          "date": { "type": "string", "format": "date", "description": "New booking date. For a meeting room, keeps the current time unless startMinute/endMinute are also sent. Must not be in the past.", "example": "2026-07-21" },
          "deskId": { "type": "string", "description": "Desk booking only — reschedule to another desk (a desk id from availability). Unknown ids return desk_not_found.", "example": "room2-desk4" },
          "startMinute": { "type": "integer", "description": "Meeting room only — new start, minutes from midnight (600 = 10:00). Aligned to the room's slotMinutes, within openMinute/closeMinute.", "example": 660 },
          "endMinute": { "type": "integer", "description": "Meeting room only — new end, minutes from midnight.", "example": 780 },
          "personName": { "type": "string", "nullable": true },
          "title": { "type": "string", "nullable": true },
          "notes": { "type": "string", "nullable": true },
          "visitorName": { "type": "string", "nullable": true, "description": "Desk booking only — visitor's name." },
          "visitorPhone": { "type": "string", "nullable": true, "description": "Desk booking only — visitor's phone." },
          "status": { "type": "string", "enum": ["booked", "confirmed", "assigned"], "description": "Meeting room: booked | confirmed. Desk: booked | assigned. Use the cancel endpoint to cancel." },
          "price": { "type": "number", "description": "Meeting room only — override the price. If omitted while rescheduling, price is recomputed from the room's hourly rate." },
          "companyName": { "type": "string", "nullable": true, "description": "Meeting room only." },
          "companyAddress": { "type": "string", "nullable": true, "description": "Meeting room only." },
          "companyVatId": { "type": "string", "nullable": true, "description": "Meeting room only." }
        }
      },
      "UpdateEnvelope": {
        "type": "object",
        "description": "Echoes the booking's new state. Meeting-room updates carry meetingRoomId/startTime/endTime/price/currency; desk updates carry deskId. `type` distinguishes them.",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "updated": { "type": "boolean", "enum": [true] },
              "id": { "type": "string", "description": "Meeting-room booking UUID or numeric desk-booking id (as a number for desk).", "example": 21808 },
              "type": { "type": "string", "enum": ["room", "desk"] },
              "meetingRoomId": { "type": "string", "format": "uuid", "description": "Meeting-room updates only." },
              "deskId": { "type": "string", "description": "Desk updates only." },
              "date": { "type": "string", "format": "date" },
              "startTime": { "type": "string", "format": "date-time", "description": "Meeting-room updates only." },
              "endTime": { "type": "string", "format": "date-time", "description": "Meeting-room updates only." },
              "status": { "type": "string" },
              "price": { "type": "number", "nullable": true, "description": "Meeting-room updates only." },
              "currency": { "type": "string", "nullable": true, "description": "Meeting-room updates only." }
            }
          },
          "error": { "type": "null" },
          "meta": { "type": "object", "properties": { "organization_id": { "type": "string" } } }
        }
      },
      "ErrorEnvelope": {
        "type": "object",
        "properties": {
          "data": { "type": "null" },
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "example": "no_desks_available" },
              "message": { "type": "string", "example": "No desks are available on 2026-07-20. Please adjust the dates." },
              "date": { "type": "string", "format": "date", "description": "Present on date-specific errors.", "example": "2026-07-20" }
            }
          }
        }
      }
    }
  },
  "security": [ { "ApiKeyBearer": [] } ]
}
