{
  "openapi": "3.0.3",
  "info": {
    "title": "CocoBook Public API",
    "version": "1.0.0",
    "description": "Property-scoped access to bookings, availability, quotes, units and the property profile. Authenticate with an API key created in CocoBook under Settings → Developer. Full guide: https://cocobook.app/developers",
    "contact": { "name": "CocoBook", "url": "https://cocobook.app/developers" }
  },
  "servers": [{ "url": "https://api.cocobook.app/v1" }],
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "Keys", "description": "The authenticated key" },
    { "name": "Property", "description": "The property the key belongs to and its units" },
    { "name": "Availability", "description": "Free and busy dates, price quotes" },
    { "name": "Bookings", "description": "Create, read, change and cancel bookings" }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": ["Keys"], "summary": "Describe the key and its property", "operationId": "getMe",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Me" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/property": {
      "get": {
        "tags": ["Property"], "summary": "Get the property profile", "operationId": "getProperty",
        "description": "Requires the property:read scope.",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Property" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "patch": {
        "tags": ["Property"], "summary": "Update the property profile", "operationId": "patchProperty",
        "description": "Requires the property:write scope. Only the fields sent are changed. Changes invalidate the cached guest site.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PropertyInput" } } } },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Property" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/units": {
      "get": {
        "tags": ["Property"], "summary": "List units", "operationId": "listUnits", "description": "Requires property:read.",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Unit" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/units/{id}": {
      "get": {
        "tags": ["Property"], "summary": "Get a unit", "operationId": "getUnit", "description": "Requires property:read.",
        "parameters": [{ "$ref": "#/components/parameters/id" }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Unit" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/availability": {
      "get": {
        "tags": ["Availability"], "summary": "Busy ranges per unit", "operationId": "getAvailability",
        "description": "Requires bookings:read. Every range inside [from, to) that is not free: live bookings, blocks (cleaning, owner stay, maintenance) and events imported from external iCal feeds (Airbnb, Booking.com). Cancelled bookings are free. Window of at most 366 days.",
        "parameters": [
          { "name": "from", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "to", "in": "query", "required": true, "schema": { "type": "string", "format": "date" }, "description": "Exclusive" },
          { "name": "unit_id", "in": "query", "schema": { "type": "string" }, "description": "Limit to one unit" }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Availability" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/quote": {
      "get": {
        "tags": ["Availability"], "summary": "Price a stay", "operationId": "getQuote",
        "description": "Requires bookings:read. Each night is priced from the property's seasons and weekend days: inside a season, the unit's weekday or weekend rate for that season when set, else the unit's base (weekday/weekend) rate times the season multiplier; outside seasons, the unit's base weekday or weekend rate. Plus the breakfast line. Creating a booking without a price uses the same calculation.",
        "parameters": [
          { "name": "unit_id", "in": "query", "required": true, "schema": { "type": "string" } },
          { "name": "check_in", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "check_out", "in": "query", "required": true, "schema": { "type": "string", "format": "date" } },
          { "name": "breakfast_guests", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 99 } }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Quote" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      }
    },
    "/bookings": {
      "get": {
        "tags": ["Bookings"], "summary": "List bookings", "operationId": "listBookings",
        "description": "Requires bookings:read. Sorted by check-in. Blocks are left out unless include_blocks=true or status names them.",
        "parameters": [
          { "name": "from", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Only bookings that end after this date" },
          { "name": "to", "in": "query", "schema": { "type": "string", "format": "date" }, "description": "Only bookings that start before this date" },
          { "name": "status", "in": "query", "schema": { "type": "string" }, "description": "Comma-separated: confirmed, pending, conflict, cancelled, cleaning, owner, blocked" },
          { "name": "unit_id", "in": "query", "schema": { "type": "string" } },
          { "name": "updated_since", "in": "query", "schema": { "type": "string", "format": "date-time" }, "description": "For incremental sync" },
          { "name": "include_blocks", "in": "query", "schema": { "type": "boolean", "default": false } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingList" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }
        }
      },
      "post": {
        "tags": ["Bookings"], "summary": "Create a booking or a block", "operationId": "createBooking",
        "description": "Requires bookings:write. The unit must be free for the whole stay; overlaps answer 409. Omit price to have it computed like /quote. Send an Idempotency-Key header to make retries safe: a repeat with the same key returns the booking created the first time (header Idempotent-Replayed: true) instead of a duplicate.",
        "parameters": [{ "name": "Idempotency-Key", "in": "header", "schema": { "type": "string", "maxLength": 255 } }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingInput" } } } },
        "responses": {
          "201": { "description": "Created", "headers": { "Location": { "schema": { "type": "string" } } }, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "200": { "description": "Replayed from the Idempotency-Key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "409": { "$ref": "#/components/responses/Conflict" }
        }
      }
    },
    "/bookings/{id}": {
      "get": {
        "tags": ["Bookings"], "summary": "Get a booking", "operationId": "getBooking", "description": "Requires bookings:read.",
        "parameters": [{ "$ref": "#/components/parameters/id" }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "tags": ["Bookings"], "summary": "Change a booking", "operationId": "patchBooking",
        "description": "Requires bookings:write. Only the fields sent change. Moving the dates or the unit re-checks overlaps and, unless a price is sent, re-prices the stay. status: cancelled behaves like a cancel.",
        "parameters": [{ "$ref": "#/components/parameters/id" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BookingInput" } } } },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" }, "404": { "$ref": "#/components/responses/NotFound" }, "409": { "$ref": "#/components/responses/Conflict" }
        }
      },
      "delete": {
        "tags": ["Bookings"], "summary": "Cancel a booking", "operationId": "deleteBooking",
        "description": "Requires bookings:write. Same as POST /bookings/{id}/cancel: the booking is kept with status cancelled and its parking is released. Cancelling twice is fine.",
        "parameters": [{ "$ref": "#/components/parameters/id" }],
        "responses": {
          "200": { "description": "Cancelled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/bookings/{id}/cancel": {
      "post": {
        "tags": ["Bookings"], "summary": "Cancel a booking", "operationId": "cancelBooking", "description": "Requires bookings:write.",
        "parameters": [{ "$ref": "#/components/parameters/id" }],
        "responses": {
          "200": { "description": "Cancelled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Booking" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": { "type": "http", "scheme": "bearer", "description": "Authorization: Bearer cb_live_… (or the X-API-Key header)" }
    },
    "parameters": {
      "id": { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The resource id (a UUID)" }
    },
    "responses": {
      "BadRequest": { "description": "Invalid input", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "validation_error", "message": "One or more fields are invalid.", "details": { "check_out": "must be after check_in" } } } } } },
      "Unauthorized": { "description": "Missing or invalid key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "invalid_api_key", "message": "The API key is not valid." } } } } },
      "Forbidden": { "description": "The key lacks the scope", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "insufficient_scope", "message": "This key does not have the 'bookings:write' scope." } } } } },
      "NotFound": { "description": "No such resource in this property", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Conflict": { "description": "The unit is not free", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": { "code": "unit_unavailable", "message": "The unit already has a booking overlapping these dates.", "details": { "conflicting_booking_ids": ["0b1c…"] } } } } } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "object", "required": ["code", "message"], "properties": {
          "code": { "type": "string", "enum": ["missing_api_key", "invalid_api_key", "insufficient_scope", "validation_error", "invalid_json", "not_found", "unit_unavailable", "unit_blocked", "rate_limited", "internal_error"] },
          "message": { "type": "string" },
          "details": { "type": "object", "additionalProperties": true }
        } } }
      },
      "Me": {
        "type": "object",
        "properties": {
          "key": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "prefix": { "type": "string" }, "scopes": { "type": "array", "items": { "type": "string" } }, "created_at": { "type": "string", "format": "date-time" }, "expires_at": { "type": "string", "format": "date-time" } } },
          "property": { "$ref": "#/components/schemas/Property" }
        }
      },
      "Property": {
        "type": "object",
        "properties": {
          "id": { "type": "string" }, "name": { "type": "string" }, "currency": { "type": "string", "example": "EUR" },
          "location": { "type": "object", "properties": { "country": { "type": "string", "description": "ISO 3166-1 alpha-2" }, "country_name": { "type": "string" }, "region": { "type": "string" }, "town": { "type": "string" }, "label": { "type": "string" } } },
          "image_url": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "PropertyInput": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "maxLength": 200 },
          "currency": { "type": "string", "enum": ["EUR", "USD", "GBP", "TRY", "BGN", "RON", "CHF"] },
          "location": { "type": "object", "properties": { "country": { "type": "string", "description": "ISO 3166-1 alpha-2" }, "region": { "type": "string", "maxLength": 80 }, "town": { "type": "string", "maxLength": 80 } } }
        }
      },
      "Unit": {
        "type": "object",
        "properties": {
          "id": { "type": "string" }, "name": { "type": "string" }, "type": { "type": "string", "enum": ["suite", "studio", "apartment", "villa", "room", "loft"] },
          "capacity": { "type": "integer" }, "floor": { "type": "string" }, "base_price": { "type": "number", "description": "Weekday rate outside seasons" }, "weekend_price": { "type": "number", "description": "Rate on the property's weekend days; 0 means same as base_price" },
          "breakfast": { "type": "boolean" }, "breakfast_price": { "type": "number", "description": "Per person per night; 0 means included" }
        }
      },
      "UnitRef": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } },
      "Guest": {
        "type": "object",
        "properties": { "name": { "type": "string", "maxLength": 200 }, "email": { "type": "string", "format": "email" }, "phone": { "type": "string", "maxLength": 50 }, "nationality": { "type": "string", "description": "ISO 3166-1 alpha-2" } }
      },
      "Booking": {
        "type": "object",
        "properties": {
          "id": { "type": "string" }, "code": { "type": "string", "example": "B-4821" },
          "unit": { "$ref": "#/components/schemas/UnitRef" }, "guest": { "$ref": "#/components/schemas/Guest" },
          "check_in": { "type": "string", "format": "date" }, "check_out": { "type": "string", "format": "date" }, "nights": { "type": "integer" },
          "status": { "type": "string", "enum": ["confirmed", "pending", "conflict", "cancelled", "cleaning", "owner", "blocked"] },
          "source": { "type": "string", "enum": ["direct", "airbnb", "booking_com", "agoda", "referral"] },
          "price": { "type": "number", "description": "Total for the stay, breakfast included" }, "deposit": { "type": "number" }, "currency": { "type": "string" },
          "breakfast": { "type": "object", "properties": { "guests": { "type": "integer" }, "price": { "type": "number" } } },
          "notes": { "type": "string" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "BookingInput": {
        "type": "object",
        "description": "On create unit_id, check_in, check_out and guest.name are required (guest.name is optional for blocks). On patch every field is optional.",
        "properties": {
          "unit_id": { "type": "string" }, "guest": { "$ref": "#/components/schemas/Guest" },
          "check_in": { "type": "string", "format": "date" }, "check_out": { "type": "string", "format": "date", "description": "At most 365 nights after check_in" },
          "status": { "type": "string", "enum": ["confirmed", "pending", "cancelled", "cleaning", "owner", "blocked"], "default": "confirmed" },
          "source": { "type": "string", "enum": ["direct", "airbnb", "booking_com", "agoda", "referral"], "default": "direct" },
          "price": { "type": "number", "minimum": 0, "description": "Omit to compute from the seasons" }, "deposit": { "type": "number", "minimum": 0 },
          "breakfast_guests": { "type": "integer", "minimum": 0, "maximum": 99 }, "notes": { "type": "string", "maxLength": 4000 }
        }
      },
      "BookingList": {
        "type": "object",
        "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Booking" } }, "page": { "type": "integer" }, "per_page": { "type": "integer" }, "total": { "type": "integer" } }
      },
      "Availability": {
        "type": "object",
        "properties": {
          "from": { "type": "string", "format": "date" }, "to": { "type": "string", "format": "date" },
          "units": { "type": "array", "items": { "type": "object", "properties": {
            "unit": { "$ref": "#/components/schemas/UnitRef" },
            "busy": { "type": "array", "items": { "type": "object", "properties": {
              "from": { "type": "string", "format": "date" }, "to": { "type": "string", "format": "date", "description": "Exclusive" },
              "type": { "type": "string", "enum": ["booking", "block", "external"] }, "status": { "type": "string" },
              "booking_id": { "type": "string" }, "summary": { "type": "string", "description": "For external events" }
            } } }
          } } }
        }
      },
      "Quote": {
        "type": "object",
        "properties": {
          "unit": { "$ref": "#/components/schemas/UnitRef" }, "check_in": { "type": "string", "format": "date" }, "check_out": { "type": "string", "format": "date" },
          "nights": { "type": "integer" }, "currency": { "type": "string" }, "room": { "type": "number" },
          "breakfast": { "type": "object", "properties": { "guests": { "type": "integer" }, "price": { "type": "number" } } },
          "total": { "type": "number" },
          "per_night": { "type": "array", "items": { "type": "object", "properties": { "date": { "type": "string", "format": "date" }, "rate": { "type": "number" }, "season": { "type": "string" }, "weekend": { "type": "boolean" } } } }
        }
      }
    }
  }
}
