BloomBooking API

v1.0.0 · openapi.json

https://api.bloombooking.com/api/v1

API REST del centro. Es la base de todas las conectividades de BloomBooking: la web pública de reservas, el servidor MCP, los agentes de voz y de WhatsApp y las integraciones del propio centro hablan con este mismo motor.

Autenticación

Cada centro emite sus claves desde Ajustes → API. Se manda en cada petición como Authorization: Bearer bb_… (o X-API-Key). Una clave pertenece a un solo centro y lleva permisos explícitos, así que una integración de sólo lectura no puede anular citas aunque quiera.

La clave se enseña una vez, al crearla: después sólo vive su hash. Si se pierde, se revoca y se emite otra.

Horas y fechas

Los instantes van en ISO 8601 con offset (2026-09-01T09:00:00.000Z). Los rangos de días (from, to) son días civiles del centro, y las respuestas traen además la hora de pared del local y su zona IANA: quien integra no tiene por qué saber que Canarias no empieza el día a la misma hora que Madrid.

Errores

Siempre { "error": { "code", "message", "details?" } } con el código estable en code. El 409 al reservar (slot_taken) no es un fallo de la integración: es que alguien ha cogido ese hueco primero.

Límite de uso

120 peticiones por minuto y clave. Al pasarse, 429 con rate_limited.

Cuenta

La clave y el centro al que pertenece.

GET /me

Quién soy

El centro al que pertenece la clave y los permisos que lleva.

sólo clave válida

Respuesta 200: Me

Catálogo

Locales, servicios y profesionales.

GET /locations

Locales del centro

Cada local trae su zona horaria: las horas de pared se derivan de ahí.

catalog:read

Respuesta 200: LocationList

GET /services

Servicios

El catálogo completo del centro. Con bookable_online=true sólo lo que el centro publica en su web pública de reservas.

catalog:read

location_id uuid
bookable_online string

Respuesta 200: ServiceList

GET /professionals

Profesionales

Con location_id, sólo quien tiene horario en esa sede.

catalog:read

location_id uuid

Respuesta 200: ProfessionalList

Disponibilidad

Qué huecos hay libres.

GET /availability

Huecos libres de un servicio

El mismo motor que la agenda del centro: horario real del profesional × recursos libres, con el quantum y el margen entre citas del centro. La sala y el aparato no se eligen aquí: los asigna el sistema al reservar.

availability:read

service_id obligatorio uuid
from obligatorio stringFecha civil del centro, YYYY-MM-DD
to obligatorio stringFecha civil del centro, YYYY-MM-DD
professional_id uuid
limit integer

Respuesta 200: Availability

Clientes

Fichas de las clientas del centro.

GET /clients

Buscar clientes

Por texto libre (q), por teléfono (en cualquier formato: se normaliza a E.164 antes de buscar) o por correo.

clients:read

q string
phone string
email string
page integer
page_size integer

Respuesta 200: ClientList

POST /clients

Crear cliente

Si ya hay una ficha con ese teléfono o ese correo, se devuelve esa: duplicar la ficha de una clienta habitual no es una opción.

clients:write

Cuerpo: ClientInput

Respuesta 201: ClientEnvelope

GET /clients/{id}

Ficha de un cliente

clients:read

id obligatorio uuid

Respuesta 200: ClientEnvelope

Citas

Consultar, reservar y anular.

GET /appointments

Citas de un rango de días

from y to son días civiles del centro, no instantes.

appointments:read

from obligatorio stringFecha civil del centro, YYYY-MM-DD
to obligatorio stringFecha civil del centro, YYYY-MM-DD
location_id uuid
client_id uuid
professional_id uuid
status pending · confirmed · in_progress · completed · no_show · cancelled
page integer
page_size integer

Respuesta 200: AppointmentList

POST /appointments

Reservar una cita

La hora se vuelve a comprobar contra el motor antes de guardar, y el solape lo impide Postgres con una constraint de exclusión: si dos sistemas piden el mismo hueco a la vez, el segundo recibe 409 slot_taken. El cliente se indica con client_id o se describe en client (se reconoce por teléfono o correo antes de crear ficha).

appointments:write

Cuerpo: CreateAppointment

Respuesta 201: AppointmentEnvelope

GET /appointments/{id}

Una cita

appointments:read

id obligatorio uuid

Respuesta 200: AppointmentEnvelope

POST /appointments/{id}/cancel

Anular una cita

El motivo queda en la ficha y viaja en el evento appointment.cancelled.

appointments:write

id obligatorio uuid

Cuerpo: CancelAppointment

Respuesta 200: AppointmentEnvelope

Esquemas

Me
{
  "type": "object",
  "properties": {
    "tenant": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        "slug": {
          "type": "string",
          "minLength": 3,
          "maxLength": 63,
          "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
        },
        "name": {
          "type": "string"
        }
      },
      "required": [
        "id",
        "slug",
        "name"
      ]
    },
    "key": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        "name": {
          "type": "string"
        },
        "prefix": {
          "type": "string"
        },
        "scopes": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "catalog:read",
              "availability:read",
              "clients:read",
              "clients:write",
              "appointments:read",
              "appointments:write"
            ]
          }
        },
        "expires_at": {
          "anyOf": [
            {
              "type": "string",
              "format": "date-time",
              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "id",
        "name",
        "prefix",
        "scopes",
        "expires_at"
      ]
    }
  },
  "required": [
    "tenant",
    "key"
  ]
}
Location
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "name": {
      "type": "string"
    },
    "timezone": {
      "type": "string"
    },
    "address_line": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "postal_code": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "city": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "phone": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "id",
    "name",
    "timezone",
    "address_line",
    "postal_code",
    "city",
    "phone",
    "email"
  ]
}
LocationList
{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Location"
      }
    }
  },
  "required": [
    "items"
  ]
}
Service
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "name": {
      "type": "string"
    },
    "group_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "group_name": {
      "type": "string"
    },
    "location_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "duration_minutes": {
      "type": "integer",
      "minimum": -9007199254740991,
      "maximum": 9007199254740991
    },
    "price_cents": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "is_collective": {
      "type": "boolean"
    },
    "max_capacity": {
      "anyOf": [
        {
          "type": "integer",
          "minimum": -9007199254740991,
          "maximum": 9007199254740991
        },
        {
          "type": "null"
        }
      ]
    },
    "bookable_online": {
      "type": "boolean"
    }
  },
  "required": [
    "id",
    "name",
    "group_id",
    "group_name",
    "location_id",
    "duration_minutes",
    "price_cents",
    "is_collective",
    "max_capacity",
    "bookable_online"
  ]
}
ServiceList
{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Service"
      }
    }
  },
  "required": [
    "items"
  ]
}
Professional
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "name": {
      "type": "string"
    },
    "is_active": {
      "type": "boolean"
    }
  },
  "required": [
    "id",
    "name",
    "is_active"
  ]
}
ProfessionalList
{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Professional"
      }
    }
  },
  "required": [
    "items"
  ]
}
Slot
{
  "type": "object",
  "properties": {
    "starts_at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
    },
    "ends_at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
    },
    "local_date": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
      "description": "Fecha civil del centro, `YYYY-MM-DD`"
    },
    "local_time": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
    },
    "professional_id": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        {
          "type": "null"
        }
      ]
    },
    "professional_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "room_id": {
      "anyOf": [
        {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        {
          "type": "null"
        }
      ]
    },
    "room_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "starts_at",
    "ends_at",
    "local_date",
    "local_time",
    "professional_id",
    "professional_name",
    "room_id",
    "room_name"
  ]
}
Availability
{
  "type": "object",
  "properties": {
    "service": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        "name": {
          "type": "string"
        },
        "duration_minutes": {
          "type": "integer",
          "minimum": -9007199254740991,
          "maximum": 9007199254740991
        },
        "price_cents": {
          "type": "integer",
          "minimum": 0,
          "maximum": 9007199254740991
        },
        "location_id": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        "timezone": {
          "type": "string"
        }
      },
      "required": [
        "id",
        "name",
        "duration_minutes",
        "price_cents",
        "location_id",
        "timezone"
      ]
    },
    "days": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Fecha civil del centro, `YYYY-MM-DD`"
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Slot"
            }
          }
        },
        "required": [
          "date",
          "slots"
        ]
      }
    },
    "total_slots": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "empty_reason": {
      "type": "string",
      "enum": [
        "none",
        "no_professionals",
        "no_rooms",
        "no_equipment",
        "no_working_hours",
        "all_busy"
      ]
    }
  },
  "required": [
    "service",
    "days",
    "total_slots",
    "empty_reason"
  ]
}
Client
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "first_name": {
      "type": "string"
    },
    "last_name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "anyOf": [
        {
          "type": "string",
          "format": "email",
          "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
        },
        {
          "type": "null"
        }
      ]
    },
    "phone": {
      "anyOf": [
        {
          "type": "string",
          "pattern": "^\\+[1-9]\\d{6,14}$"
        },
        {
          "type": "null"
        }
      ]
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
    }
  },
  "required": [
    "id",
    "first_name",
    "last_name",
    "email",
    "phone",
    "created_at"
  ]
}
ClientList
{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Client"
      }
    },
    "total": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "page": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "page_size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "items",
    "total",
    "page",
    "page_size"
  ]
}
ClientInput
{
  "type": "object",
  "properties": {
    "first_name": {
      "type": "string",
      "minLength": 2,
      "maxLength": 80
    },
    "last_name": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 80
        },
        {
          "type": "null"
        }
      ]
    },
    "email": {
      "anyOf": [
        {
          "anyOf": [
            {
              "type": "string",
              "format": "email",
              "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
            },
            {
              "type": "string",
              "const": ""
            }
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "phone": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 40
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "first_name"
  ]
}
ClientEnvelope
{
  "type": "object",
  "properties": {
    "client": {
      "$ref": "#/components/schemas/Client"
    }
  },
  "required": [
    "client"
  ]
}
Appointment
{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "status": {
      "type": "string",
      "enum": [
        "pending",
        "confirmed",
        "in_progress",
        "completed",
        "no_show",
        "cancelled"
      ]
    },
    "source": {
      "type": "string",
      "enum": [
        "staff",
        "online",
        "api",
        "assistant"
      ]
    },
    "starts_at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
    },
    "ends_at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
    },
    "local_date": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
      "description": "Fecha civil del centro, `YYYY-MM-DD`"
    },
    "local_time": {
      "type": "string",
      "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
    },
    "timezone": {
      "type": "string"
    },
    "location_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "client": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        "name": {
          "type": "string"
        },
        "phone": {
          "anyOf": [
            {
              "type": "string",
              "pattern": "^\\+[1-9]\\d{6,14}$"
            },
            {
              "type": "null"
            }
          ]
        }
      },
      "required": [
        "id",
        "name",
        "phone"
      ]
    },
    "service": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "format": "uuid",
          "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
        },
        "name": {
          "type": "string"
        },
        "duration_minutes": {
          "type": "integer",
          "minimum": -9007199254740991,
          "maximum": 9007199254740991
        },
        "price_cents": {
          "type": "integer",
          "minimum": 0,
          "maximum": 9007199254740991
        }
      },
      "required": [
        "id",
        "name",
        "duration_minutes",
        "price_cents"
      ]
    },
    "professional": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            },
            "name": {
              "type": "string"
            }
          },
          "required": [
            "id",
            "name"
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "room": {
      "anyOf": [
        {
          "type": "object",
          "properties": {
            "id": {
              "type": "string",
              "format": "uuid",
              "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
            },
            "name": {
              "type": "string"
            }
          },
          "required": [
            "id",
            "name"
          ]
        },
        {
          "type": "null"
        }
      ]
    },
    "notes": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "cancel_reason": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ]
    },
    "created_at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
    }
  },
  "required": [
    "id",
    "status",
    "source",
    "starts_at",
    "ends_at",
    "local_date",
    "local_time",
    "timezone",
    "location_id",
    "client",
    "service",
    "professional",
    "room",
    "notes",
    "cancel_reason",
    "created_at"
  ]
}
AppointmentList
{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/Appointment"
      }
    },
    "total": {
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "page": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    },
    "page_size": {
      "type": "integer",
      "minimum": 1,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "items",
    "total",
    "page",
    "page_size"
  ]
}
AppointmentEnvelope
{
  "type": "object",
  "properties": {
    "appointment": {
      "$ref": "#/components/schemas/Appointment"
    }
  },
  "required": [
    "appointment"
  ]
}
CreateAppointment
{
  "type": "object",
  "properties": {
    "service_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "starts_at": {
      "type": "string",
      "format": "date-time",
      "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
    },
    "professional_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "client_id": {
      "type": "string",
      "format": "uuid",
      "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
    },
    "client": {
      "$ref": "#/components/schemas/ClientInput"
    },
    "status": {
      "default": "confirmed",
      "type": "string",
      "enum": [
        "pending",
        "confirmed"
      ]
    },
    "notes": {
      "anyOf": [
        {
          "type": "string",
          "maxLength": 2000
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "required": [
    "service_id",
    "starts_at"
  ]
}
CancelAppointment
{
  "type": "object",
  "properties": {
    "reason": {
      "type": "string",
      "minLength": 3,
      "maxLength": 500
    }
  },
  "required": [
    "reason"
  ]
}
Error
{
  "type": "object",
  "properties": {
    "error": {
      "type": "object",
      "properties": {
        "code": {
          "type": "string"
        },
        "message": {
          "type": "string"
        },
        "details": {}
      },
      "required": [
        "code",
        "message"
      ]
    }
  },
  "required": [
    "error"
  ]
}