{
  "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 today, 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. 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), returning a booking_group_id. Returns 402 when the space takes card payment for whole-day rooms online. 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": {
          "201": { "description": "Booking created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SingleBookingEnvelope" } } } },
          "400": { "description": "Validation error — `missing_visitor_name`, `missing_visitor_email`, `invalid_date`, `invalid_range`, `past_date`, `range_too_long`, `whole_day_not_available`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "402": { "description": "This space takes card payment for whole-day rooms online (`payment_required`) — API checkout is coming soon.", "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" } } } }
        }
      }
    },
    "/public/{slug}/office-requests": {
      "post": {
        "tags": ["Public"],
        "summary": "Request a private office",
        "description": "Registers interest in a private office — creates a lead for the operator, not a confirmed booking. companyName is required. Returns 402 when the space takes card payment for an available office online. 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": {
          "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`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorEnvelope" } } } },
          "402": { "description": "This space takes card payment for an available office online (`payment_required`) — API checkout is coming soon.", "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" } } } }
        }
      }
    },
    "/": {
      "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" } } }
          }
        }
      }
    }
  },
  "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" },
          "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 }
        }
      },
      "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 },
          "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 }
        }
      },
      "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" } } }
        }
      },
      "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" }
        }
      },
      "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": [] } ]
}
