{
  "openapi": "3.1.0",
  "info": {
    "title": "API pública de Programa Taller",
    "version": "1.0.0",
    "description": "API REST de la instancia del taller. Autenticación por clave (Bearer o X-Api-Key), respuestas JSON, paginación por cursor, fechas ISO 8601 en UTC e importes en euros con dos decimales.\n\nClaves de cliente: el taller puede crear claves limitadas a un único cliente (por ejemplo, una flota o una empresa). Con ellas cada listado devuelve solo los datos de ese cliente, un id de otro cliente responde 404, las operaciones de todo el taller responden 403 client_key_forbidden y los webhooks solo reciben eventos de ese cliente. GET /ping indica en key.client a qué cliente está limitada.",
    "x-client-keys": {
      "forbiddenScopes": [
        "invoices:write",
        "stock:write",
        "settings:write",
        "reports:read"
      ],
      "description": "Claves de cliente: el taller puede crear claves limitadas a un único cliente (por ejemplo, una flota o una empresa). Con ellas cada listado devuelve solo los datos de ese cliente, un id de otro cliente responde 404, las operaciones de todo el taller responden 403 client_key_forbidden y los webhooks solo reciben eventos de ese cliente. GET /ping indica en key.client a qué cliente está limitada."
    },
    "x-languages": [
      "es",
      "en",
      "ca",
      "pt",
      "fr",
      "bg"
    ],
    "x-sandbox": {
      "url": "https://demo.programa-taller.es",
      "signup": "https://demo.programa-taller.es/desarrolladores-sandbox",
      "resetEveryHours": 2,
      "keyDays": 30
    },
    "x-guides": [
      {
        "id": "getting-started",
        "title": "Primeros pasos",
        "intro": "El mismo recorrido que el tutorial «Empieza con la API paso a paso» de Desarrolladores, para quien programa. Crea una clave de pruebas (pt_test_…) con permisos de solo lectura, comprueba la conexión, lee datos reales y, cuando la integración esté lista, pasa a una clave live con los permisos justos. Los avisos automáticos (webhooks) y las escrituras requieren el plan Developer o superior.",
        "steps": [
          {
            "title": "1. Crea una clave en «Mis claves»",
            "operationId": null,
            "method": null,
            "path": null,
            "body": null,
            "note": "Elige el entorno de pruebas y solo permisos de lectura (por ejemplo budgets:read). La clave completa se muestra una sola vez: guárdala en un gestor de contraseñas y envíala en la cabecera Authorization: Bearer."
          },
          {
            "title": "2. Comprueba la conexión",
            "operationId": "ping",
            "method": "GET",
            "path": "/api/v1/ping",
            "body": null,
            "note": "Responde con el nombre de la clave, sus permisos y el entorno. Un 401 significa clave no válida; un 403, que falta un permiso."
          },
          {
            "title": "3. Lee tus datos reales",
            "operationId": "listBudgets",
            "method": "GET",
            "path": "/api/v1/budgets",
            "body": "?limit=3",
            "note": "Los listados se paginan por cursor: repite la llamada con cursor=<next_cursor> hasta que has_more sea false. Cada respuesta trae X-RateLimit-Remaining con las peticiones que quedan en la ventana."
          },
          {
            "title": "4. Recibe avisos automáticos",
            "operationId": "createWebhook",
            "method": "POST",
            "path": "/api/v1/webhooks",
            "body": "{ \"url\": \"https://tu-sistema.com/avisos-del-taller\", \"events\": [\"lead.accepted\", \"vehicle.ready\"] }",
            "note": "También se crean desde la pestaña Webhooks sin programar. Guarda el secreto de firma y verifica X-PT-Signature en cada entrega; responde 2xx en menos de 5 segundos."
          },
          {
            "title": "5. Pasa a producción",
            "operationId": null,
            "method": null,
            "path": null,
            "body": null,
            "note": "Antes, si quieres, prueba escrituras, aceptaciones, facturas y mensajes sin riesgo en el entorno de pruebas (https://demo.programa-taller.es/desarrolladores-sandbox): datos ficticios y nada sale de verdad. Crea una clave live con los permisos mínimos y, si vas a escribir datos, añade Idempotency-Key a cada POST. Los límites y el plan de API del taller se ven en la pestaña Consumo; los planes Developer y Business se contratan desde el propio programa."
          }
        ]
      },
      {
        "id": "budget-and-appointment",
        "title": "Guía: crear un presupuesto y proponer cita",
        "intro": "Flujo típico de un CRM o una web de reservas. Todas las peticiones POST llevan Idempotency-Key (un UUID nuevo por operación; repítelo solo al reintentar la misma). Necesitas una clave live con clients:write, vehicles:write, budgets:write, appointments:read y appointments:write.",
        "steps": [
          {
            "title": "1. Cliente: crearlo o recuperar el existente",
            "operationId": "createClient",
            "method": "POST",
            "path": "/api/v1/clients",
            "body": "{ \"name\": \"Laura Gómez\", \"phone\": \"600111222\", \"email\": \"laura@ejemplo.com\" }",
            "note": "Con ?on_conflict=return_existing, si el teléfono, email o NIF ya existen recibes ese cliente (200) en vez de 409."
          },
          {
            "title": "2. Vehículo del cliente",
            "operationId": "createVehicle",
            "method": "POST",
            "path": "/api/v1/vehicles",
            "body": "{ \"client_id\": 1204, \"plate\": \"1234KLM\", \"brand\": \"Seat\", \"model\": \"León\" }",
            "note": "Si la matrícula es de otro cliente: 409 vehicle_belongs_to_other_client (el taller decide)."
          },
          {
            "title": "3. Presupuesto con sus partidas",
            "operationId": "createBudget",
            "method": "POST",
            "path": "/api/v1/budgets",
            "body": "{ \"client_id\": 1204, \"vehicle_id\": 871, \"lines\": [{ \"description\": \"Cambio de aceite y filtro\", \"quantity\": 1, \"unit_price\": 65 }] }",
            "note": "La respuesta trae los totales calculados. El cliente acepta y firma desde el enlace que le envía el taller (la API nunca da ese enlace); si te lo confirma a ti por otra vía, acéptalo en su nombre con POST /budgets/{id}/accept (permiso budgets:accept)."
          },
          {
            "title": "4. Huecos libres",
            "operationId": "getAvailability",
            "method": "GET",
            "path": "/api/v1/appointments/availability",
            "body": "?from=2026-10-14&to=2026-10-18&duration_minutes=60",
            "note": null
          },
          {
            "title": "5. Proponer la cita",
            "operationId": "createAppointment",
            "method": "POST",
            "path": "/api/v1/appointments",
            "body": "{ \"budget_id\": 1234, \"start\": \"2026-10-14T09:00:00+02:00\", \"duration_minutes\": 60 }",
            "note": "En modo «proponer» queda status=proposed hasta que el taller la confirma (recibirás appointment.confirmed por webhook). Si el hueco se ha ocupado: 409 slot_unavailable con alternativas."
          }
        ]
      },
      {
        "id": "external-bot",
        "title": "Guía: tu bot o IA en los canales del taller",
        "intro": "Tu asistente contesta por WhatsApp, SMS, chat web o email en lugar del asistente del programa. Necesitas una clave live con communications:read, messages:send, settings:write y webhooks:manage (plan Business o Partner). Pruébalo antes en el entorno de pruebas con POST /sandbox/simulate/message.",
        "steps": [
          {
            "title": "1. Suscríbete a message.received",
            "operationId": "createWebhook",
            "method": "POST",
            "path": "/api/v1/webhooks",
            "body": "{ \"url\": \"https://tu-bot.com/taller\", \"events\": [\"message.received\"] }",
            "note": "Cada mensaje del cliente llega firmado con la clave de la conversación (key), el cliente, el presupuesto activo, el texto y los adjuntos."
          },
          {
            "title": "2. Activa el modo asistente externo",
            "operationId": "updateAssistantSettings",
            "method": "PUT",
            "path": "/api/v1/settings/assistant",
            "body": "{ \"external_channels\": [\"whatsapp\", \"chat\"], \"fallback_minutes\": 10 }",
            "note": "En esos canales el asistente del programa deja de contestar. Si no respondes a tiempo, o tu clave o tu webhook dejan de funcionar, vuelve a contestar él a ese mensaje."
          },
          {
            "title": "3. Lee el hilo si lo necesitas",
            "operationId": "listConversationMessages",
            "method": "GET",
            "path": "/api/v1/conversations/{key}/messages",
            "body": null,
            "note": null
          },
          {
            "title": "4. Responde",
            "operationId": "sendConversationMessage",
            "method": "POST",
            "path": "/api/v1/conversations/{key}/messages",
            "body": "{ \"text\": \"Mañana a las 9:00 tenemos hueco. ¿Se la reservo?\" }",
            "note": "Fuera de la ventana de 24 h de WhatsApp envía template_id (GET /messages/templates). Para cerrar la venta: POST /appointments, POST /budgets/{id}/accept o POST /budgets/{id}/invoice según tus permisos."
          }
        ]
      },
      {
        "id": "fleet-client-key",
        "title": "Guía: flotas y renting con una clave de cliente",
        "intro": "Para que una flota, una empresa de renting o un cliente con muchos vehículos conecte sus sistemas con el taller sin ver los datos de los demás clientes, el taller le crea una clave limitada a su ficha de cliente («Limitar a un cliente» al crearla en Mis claves). Con ella cada listado devuelve solo sus datos, un id de otro cliente responde 404, las operaciones de todo el taller responden 403 client_key_forbidden y sus webhooks solo reciben sus eventos.",
        "steps": [
          {
            "title": "1. Comprueba a qué cliente está limitada",
            "operationId": "ping",
            "method": "GET",
            "path": "/api/v1/ping",
            "body": null,
            "note": "key.client trae el id y el nombre del cliente; en una clave del taller es null."
          },
          {
            "title": "2. Sus vehículos",
            "operationId": "listVehicles",
            "method": "GET",
            "path": "/api/v1/vehicles",
            "body": "?limit=200",
            "note": "Solo salen los vehículos de su ficha. Para dar de alta uno nuevo, POST /vehicles con su propio client_id."
          },
          {
            "title": "3. Pedir un presupuesto",
            "operationId": "createBudget",
            "method": "POST",
            "path": "/api/v1/budgets",
            "body": "{ \"client_id\": 1204, \"vehicle_id\": 871, \"public_notes\": \"Revisión de los 60.000 km\" }",
            "note": "client_id debe ser el de la clave (si no, 403 client_key_forbidden). El taller lo completa y lo envía; la flota lo acepta o rechaza con POST /budgets/{id}/accept o /reject (permiso budgets:accept)."
          },
          {
            "title": "4. Proponer cita y seguir el estado",
            "operationId": "createAppointment",
            "method": "POST",
            "path": "/api/v1/appointments",
            "body": "{ \"budget_id\": 1234, \"start\": \"2026-10-20T09:00:00+02:00\", \"duration_minutes\": 60 }",
            "note": "Suscribe un webhook a lead.status_changed, vehicle.ready e invoice.issued para enterarte al momento; solo llegarán los de sus vehículos."
          }
        ]
      },
      {
        "id": "sandbox",
        "title": "Entorno de pruebas",
        "intro": "https://demo.programa-taller.es es una instancia con datos ficticios que se restauran cada 2 horas. Toda clave allí es de pruebas con todos los permisos: puede escribir, aceptar, facturar y «enviar», pero nada sale de verdad (los envíos se registran como simulados) y los webhooks se emiten igual para probar el ciclo completo. Tus claves, webhooks y registro de peticiones se conservan entre reinicios.",
        "steps": [
          {
            "title": "1. Pide tu clave",
            "operationId": "createSandboxKey",
            "method": "POST",
            "path": "/api/v1/sandbox/keys",
            "body": "{ \"email\": \"dev@ejemplo.com\", \"company\": \"Integraciones Ejemplo SL\", \"name\": \"Ana Pérez\" }",
            "note": "También desde la página https://demo.programa-taller.es/desarrolladores-sandbox. La clave se muestra una sola vez y vale 30 días."
          },
          {
            "title": "2. Simula un cliente que escribe",
            "operationId": "simulateInboundMessage",
            "method": "POST",
            "path": "/api/v1/sandbox/simulate/message",
            "body": "{ \"channel\": \"whatsapp\", \"from\": \"+34600111222\", \"text\": \"¿Tienen hueco mañana?\" }",
            "note": null
          },
          {
            "title": "3. Cambia a producción",
            "operationId": null,
            "method": null,
            "path": null,
            "body": null,
            "note": "Cambia la URL base por la de la instancia del taller y usa una clave live creada en su sección Desarrolladores."
          }
        ]
      }
    ]
  },
  "servers": [
    {
      "url": "https://tu-taller.example",
      "description": "Instancia del taller (producción)"
    },
    {
      "url": "https://demo.programa-taller.es",
      "description": "Entorno de pruebas: datos ficticios que se restauran cada 2 horas, toda clave es de pruebas con todos los permisos y nada sale de verdad. Pide tu clave en https://demo.programa-taller.es/desarrolladores-sandbox"
    }
  ],
  "tags": [
    {
      "name": "General",
      "x-area": "general"
    },
    {
      "name": "Clientes",
      "x-area": "clients"
    },
    {
      "name": "Vehículos",
      "x-area": "vehicles"
    },
    {
      "name": "Presupuestos",
      "x-area": "budgets"
    },
    {
      "name": "Facturas",
      "x-area": "invoices"
    },
    {
      "name": "Citas",
      "x-area": "appointments"
    },
    {
      "name": "Catálogo",
      "x-area": "catalog"
    },
    {
      "name": "Recambios",
      "x-area": "stock"
    },
    {
      "name": "Comunicaciones",
      "x-area": "communications"
    },
    {
      "name": "Conversaciones",
      "x-area": "messages"
    },
    {
      "name": "Webhooks",
      "x-area": "webhooks"
    },
    {
      "name": "Ajustes",
      "x-area": "settings"
    },
    {
      "name": "Entorno de pruebas",
      "x-area": "sandbox"
    }
  ],
  "paths": {
    "/api/v1/ping": {
      "get": {
        "operationId": "ping",
        "tags": [
          "General"
        ],
        "summary": "Probar la conexión",
        "description": "Devuelve el nombre de la clave, su entorno, sus permisos, el estado de sus límites y, si es una clave de cliente, el cliente al que está limitada (key.client; null en una clave del taller). Vale cualquier clave válida.\n\nCualquier clave válida, sin permiso concreto.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-client-key": "open",
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ping"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workshop": {
      "get": {
        "operationId": "getWorkshop",
        "tags": [
          "General"
        ],
        "summary": "Datos públicos del taller",
        "description": "Nombre, razón social, CIF, dirección, contacto, horario semanal de la agenda, modo de reserva de citas, festivos próximos y zona horaria.\n\nCualquier clave válida, sin permiso concreto.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-client-key": "open",
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workshop"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "tags": [
          "General"
        ],
        "summary": "Especificación OpenAPI 3.1",
        "description": "Fichero generado desde este mismo catálogo. Sin autenticación. Admite ?lang=es|en|ca|pt|fr|bg para los textos.\n\nSin autenticación.",
        "security": [],
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Idioma de las descripciones.",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en",
                "ca",
                "pt",
                "fr",
                "bg"
              ],
              "default": "es"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Documento OpenAPI 3.1"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/clients": {
      "get": {
        "operationId": "listClients",
        "tags": [
          "Clientes"
        ],
        "summary": "Listar clientes",
        "description": "Ordenados por id ascendente. Los clientes borrados por RGPD aparecen anonimizados, con erased_at informado.\n\nPermiso necesario: `clients:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Busca en nombre, teléfono, email y NIF/CIF (mínimo 2 caracteres).",
            "schema": {
              "type": "string",
              "example": "laura"
            }
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "description": "Relaciones opcionales a incluir.",
            "schema": {
              "type": "string",
              "enum": [
                "vehicles"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultados por página (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opaco devuelto en next_cursor de la página anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "has_more"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Client"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor opaco para pedir la página siguiente; null si no hay más",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si quedan más resultados",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createClient",
        "tags": [
          "Clientes"
        ],
        "summary": "Crear un cliente",
        "description": "Nunca fusiona con una ficha existente: si el NIF/CIF, el email o el teléfono ya están en otro cliente responde 409 client_exists con existing_id (o 200 con ese cliente si pasas ?on_conflict=return_existing). El teléfono se guarda como en la ficha (España en 9 dígitos, otros países con prefijo) y el email en minúsculas; no se corrigen erratas. Las bajas comerciales quedan en el registro de consentimientos.\n\nPermiso necesario: `clients:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "on_conflict",
            "in": "query",
            "required": false,
            "description": "error (por defecto): 409 si ya existe. return_existing: 200 con el cliente existente.",
            "schema": {
              "type": "string",
              "enum": [
                "error",
                "return_existing"
              ],
              "default": "error"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "Nombre y apellidos o razón social",
                    "example": "Laura Gómez"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Teléfono principal. España en 9 dígitos o con +34; otros países con su prefijo (+351…)",
                    "example": "600111222",
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "phone_secondary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Segundo teléfono",
                    "example": null,
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email (no se corrige: se valida el formato)",
                    "example": "laura@ejemplo.com",
                    "format": "email"
                  },
                  "billing_email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email para las facturas, si es otro",
                    "example": null,
                    "format": "email"
                  },
                  "tax_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 20,
                    "description": "NIF/CIF/NIE",
                    "example": "12345678Z",
                    "pattern": "^[A-Za-z0-9\\s.\\-]{3,20}$"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 300,
                    "example": "C/ Mayor 12"
                  },
                  "city": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "example": "Barcelona"
                  },
                  "zip": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 10,
                    "example": "08001",
                    "pattern": "^[A-Za-z0-9\\s\\-]{3,10}$"
                  },
                  "province": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "example": "Barcelona"
                  },
                  "country": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 60,
                    "example": "ES"
                  },
                  "preferred_language": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "es",
                      "ca",
                      "en",
                      "pt",
                      "fr",
                      "bg",
                      null
                    ],
                    "description": "Idioma de las comunicaciones",
                    "example": "es"
                  },
                  "preferred_contact_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp",
                      "phone",
                      null
                    ],
                    "description": "Canal preferido",
                    "example": "whatsapp"
                  },
                  "marketing_opt_out": {
                    "type": "boolean",
                    "description": "true: no quiere comunicaciones comerciales. Queda en el registro de consentimientos",
                    "example": false
                  },
                  "channel_opt_out": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "Sin publicidad por email",
                        "example": false
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "Sin publicidad por SMS",
                        "example": false
                      },
                      "whatsapp": {
                        "type": "boolean",
                        "description": "Sin publicidad por WhatsApp",
                        "example": false
                      },
                      "call": {
                        "type": "boolean",
                        "description": "Sin llamadas comerciales",
                        "example": false
                      }
                    },
                    "required": [],
                    "additionalProperties": false,
                    "description": "Bajas comerciales por canal (los avisos de servicio no cambian)"
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "description": "Alta de cliente"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — client_exists: Ya existe un cliente con ese teléfono, email o NIF/CIF. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/clients/{id}": {
      "get": {
        "operationId": "getClient",
        "tags": [
          "Clientes"
        ],
        "summary": "Detalle de un cliente",
        "description": "Permiso necesario: `clients:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "x-client-key": "own:client",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del cliente.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "description": "Relaciones opcionales a incluir.",
            "schema": {
              "type": "string",
              "enum": [
                "vehicles"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateClient",
        "tags": [
          "Clientes"
        ],
        "summary": "Modificar un cliente",
        "description": "Solo cambian los campos enviados (null vacía el campo). Cambiar email o teléfono está permitido y deja el valor anterior → nuevo en la actividad del cliente con el nombre de la clave; emite client.updated. No se puede poner el email, teléfono o NIF de OTRA ficha (409 client_exists). Las fichas borradas por RGPD responden 409 client_erased.\n\nPermiso necesario: `clients:write`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "x-client-key": "own:client",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del cliente.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "Nombre y apellidos o razón social",
                    "example": "Laura Gómez Ruiz"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Teléfono principal. España en 9 dígitos o con +34; otros países con su prefijo (+351…)",
                    "example": "600111222",
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "phone_secondary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Segundo teléfono",
                    "example": null,
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email (no se corrige: se valida el formato)",
                    "example": "laura@ejemplo.com",
                    "format": "email"
                  },
                  "billing_email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email para las facturas, si es otro",
                    "example": null,
                    "format": "email"
                  },
                  "tax_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 20,
                    "description": "NIF/CIF/NIE",
                    "example": "12345678Z",
                    "pattern": "^[A-Za-z0-9\\s.\\-]{3,20}$"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 300,
                    "example": "C/ Mayor 12"
                  },
                  "city": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "example": "Barcelona"
                  },
                  "zip": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 10,
                    "example": "08001",
                    "pattern": "^[A-Za-z0-9\\s\\-]{3,10}$"
                  },
                  "province": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "example": "Barcelona"
                  },
                  "country": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 60,
                    "example": "ES"
                  },
                  "preferred_language": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "es",
                      "ca",
                      "en",
                      "pt",
                      "fr",
                      "bg",
                      null
                    ],
                    "description": "Idioma de las comunicaciones",
                    "example": "es"
                  },
                  "preferred_contact_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp",
                      "phone",
                      null
                    ],
                    "description": "Canal preferido",
                    "example": "whatsapp"
                  },
                  "marketing_opt_out": {
                    "type": "boolean",
                    "description": "true: no quiere comunicaciones comerciales. Queda en el registro de consentimientos",
                    "example": false
                  },
                  "channel_opt_out": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "Sin publicidad por email",
                        "example": false
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "Sin publicidad por SMS",
                        "example": false
                      },
                      "whatsapp": {
                        "type": "boolean",
                        "description": "Sin publicidad por WhatsApp",
                        "example": false
                      },
                      "call": {
                        "type": "boolean",
                        "description": "Sin llamadas comerciales",
                        "example": false
                      }
                    },
                    "required": [],
                    "additionalProperties": false,
                    "description": "Bajas comerciales por canal (los avisos de servicio no cambian)"
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Cambios parciales: solo los campos enviados. null vacía el campo"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Client"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — client_exists: Ya existe un cliente con ese teléfono, email o NIF/CIF. | client_erased: El cliente pidió el borrado de sus datos (RGPD): su ficha no admite cambios ni altas asociadas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vehicles": {
      "get": {
        "operationId": "listVehicles",
        "tags": [
          "Vehículos"
        ],
        "summary": "Listar vehículos",
        "description": "Incluye el último kilometraje anotado y el vencimiento de la ITV.\n\nPermiso necesario: `vehicles:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "plate",
            "in": "query",
            "required": false,
            "description": "Matrícula exacta (se ignoran espacios y guiones).",
            "schema": {
              "type": "string",
              "example": "1234KLM"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filtrar por cliente.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Estado del vehículo.",
            "schema": {
              "type": "string",
              "enum": [
                "activo",
                "baja_temporal",
                "baja"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultados por página (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opaco devuelto en next_cursor de la página anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "has_more"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Vehicle"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor opaco para pedir la página siguiente; null si no hay más",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si quedan más resultados",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createVehicle",
        "tags": [
          "Vehículos"
        ],
        "summary": "Dar de alta un vehículo",
        "description": "Siempre a nombre de un cliente existente (client_id). La matrícula se normaliza (mayúsculas, sin espacios ni guiones). Si ya está en la ficha de OTRO cliente: 409 vehicle_belongs_to_other_client. Si el mismo cliente ya la tiene: 409 vehicle_exists con existing_id (o 200 con ?on_conflict=return_existing).\n\nPermiso necesario: `vehicles:write`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "on_conflict",
            "in": "query",
            "required": false,
            "description": "error (por defecto) o return_existing.",
            "schema": {
              "type": "string",
              "enum": [
                "error",
                "return_existing"
              ],
              "default": "error"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Cliente titular (debe existir)",
                    "example": 1204
                  },
                  "plate": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 15,
                    "description": "Matrícula; se guarda en mayúsculas sin espacios ni guiones",
                    "example": "1234 KLM",
                    "pattern": "^[A-Za-z0-9\\s.\\-]{2,15}$"
                  },
                  "vin": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 17,
                    "description": "Bastidor",
                    "example": "VSSZZZ5FZJR123456",
                    "pattern": "^[A-Za-z0-9]{11,17}$"
                  },
                  "brand": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 60,
                    "example": "Seat"
                  },
                  "model": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 80,
                    "example": "León"
                  },
                  "variant": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 120,
                    "example": "1.5 TSI"
                  },
                  "year": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1900,
                    "maximum": 2100,
                    "example": 2019
                  },
                  "registration_date": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date",
                    "example": "2019-03-15"
                  },
                  "fuel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": "Gasolina"
                  },
                  "transmission": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": "Manual"
                  },
                  "engine_code": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": "DADA"
                  },
                  "horsepower": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 2000,
                    "example": 130
                  },
                  "displacement": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 10,
                    "description": "Cilindrada (cc)",
                    "example": "1498",
                    "pattern": "^[0-9.,]{1,10}$"
                  },
                  "color_code": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": null
                  },
                  "environmental_label": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 10,
                    "description": "Etiqueta DGT (0, ECO, C, B)",
                    "example": "C"
                  },
                  "itv_expiry_date": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date",
                    "example": "2027-03-15"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "activo",
                      "baja_temporal",
                      "baja"
                    ],
                    "example": "activo"
                  }
                },
                "required": [
                  "client_id",
                  "plate"
                ],
                "additionalProperties": false,
                "description": "Alta de vehículo de un cliente"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Vehicle"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_exists: Ese cliente ya tiene un vehículo con esa matrícula. | vehicle_belongs_to_other_client: Esa matrícula ya está dada de alta a nombre de otro cliente. | client_erased: El cliente pidió el borrado de sus datos (RGPD): su ficha no admite cambios ni altas asociadas. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vehicles/{id}": {
      "get": {
        "operationId": "getVehicle",
        "tags": [
          "Vehículos"
        ],
        "summary": "Detalle de un vehículo",
        "description": "Permiso necesario: `vehicles:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "x-client-key": "own:vehicle",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del vehículo.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Vehicle"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateVehicle",
        "tags": [
          "Vehículos"
        ],
        "summary": "Modificar un vehículo",
        "description": "Cambios parciales. client_id no admite null: un vehículo nunca se desvincula de su titular por API (sí puede pasar a otro cliente existente).\n\nPermiso necesario: `vehicles:write`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "x-client-key": "own:vehicle",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del vehículo.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Cliente titular (debe existir)",
                    "example": 1204
                  },
                  "plate": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 15,
                    "description": "Matrícula; se guarda en mayúsculas sin espacios ni guiones",
                    "example": "1234 KLM",
                    "pattern": "^[A-Za-z0-9\\s.\\-]{2,15}$"
                  },
                  "vin": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 17,
                    "description": "Bastidor",
                    "example": "VSSZZZ5FZJR123456",
                    "pattern": "^[A-Za-z0-9]{11,17}$"
                  },
                  "brand": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 60,
                    "example": "Seat"
                  },
                  "model": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 80,
                    "example": "León"
                  },
                  "variant": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 120,
                    "example": "1.5 TSI"
                  },
                  "year": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1900,
                    "maximum": 2100,
                    "example": 2019
                  },
                  "registration_date": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date",
                    "example": "2019-03-15"
                  },
                  "fuel": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": "Gasolina"
                  },
                  "transmission": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": "Manual"
                  },
                  "engine_code": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": "DADA"
                  },
                  "horsepower": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "maximum": 2000,
                    "example": 130
                  },
                  "displacement": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 10,
                    "description": "Cilindrada (cc)",
                    "example": "1498",
                    "pattern": "^[0-9.,]{1,10}$"
                  },
                  "color_code": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "example": null
                  },
                  "environmental_label": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 10,
                    "description": "Etiqueta DGT (0, ECO, C, B)",
                    "example": "C"
                  },
                  "itv_expiry_date": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date",
                    "example": "2027-03-15"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "activo",
                      "baja_temporal",
                      "baja"
                    ],
                    "example": "activo"
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Cambios parciales del vehículo"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Vehicle"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_exists: Ese cliente ya tiene un vehículo con esa matrícula. | vehicle_belongs_to_other_client: Esa matrícula ya está dada de alta a nombre de otro cliente. | client_erased: El cliente pidió el borrado de sus datos (RGPD): su ficha no admite cambios ni altas asociadas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vehicles/{id}/history": {
      "get": {
        "operationId": "getVehicleHistory",
        "tags": [
          "Vehículos"
        ],
        "summary": "Historial del vehículo",
        "description": "Visitas al taller (todos sus presupuestos salvo los de uso interno, del más reciente al más antiguo) con estado, entrada y salida, kilometraje, importe, nº de factura y conceptos de las partidas; más las intervenciones anotadas en otros talleres. Nunca incluye costes.\n\nPermiso necesario: `vehicles:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "x-client-key": "own:vehicle",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del vehículo.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Máximo de visitas (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VehicleHistory"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets": {
      "get": {
        "operationId": "listBudgets",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Listar presupuestos",
        "description": "Nunca incluye los presupuestos de uso interno del taller ni datos de coste. El detalle (con partidas y totales) está en /budgets/{id}.\n\nPermiso necesario: `budgets:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Estado exacto (Pendiente, Enviado, Aprobado, Finalizado, Facturado, Rechazado…).",
            "schema": {
              "type": "string",
              "example": "Aprobado"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filtrar por cliente.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "vehicle_id",
            "in": "query",
            "required": false,
            "description": "Filtrar por vehículo.",
            "schema": {
              "type": "integer",
              "example": 871
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Creados a partir de esta fecha.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Creados hasta esta fecha.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "description": "Solo registros modificados a partir de esta fecha (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01T00:00:00Z"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultados por página (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opaco devuelto en next_cursor de la página anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "has_more"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Budget"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor opaco para pedir la página siguiente; null si no hay más",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si quedan más resultados",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createBudget",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Crear un presupuesto",
        "description": "Entra en «Pendiente» con canal «API», igual que un alta desde el programa (registro, hito de apertura, webhook lead.created). Los totales se calculan en el servidor con el impuesto del taller; si una partida no trae tax_rate usa el del taller. El vehículo, si se indica, debe ser del cliente. No admite categorías de uso interno. Con notify_client=true (y permiso communications:send) se envía al cliente el acuse con su enlace de seguimiento.\n\nPermiso necesario: `budgets:write`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "client_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Cliente (debe existir)",
                    "example": 1204
                  },
                  "vehicle_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Vehículo del cliente",
                    "example": 871
                  },
                  "category_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Categoría de servicio (GET /catalog/services)",
                    "example": 5
                  },
                  "subcategory_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Subcategoría de esa categoría",
                    "example": 51
                  },
                  "client_reference": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "description": "Referencia del cliente (pedido, siniestro…) que verá en la factura",
                    "example": "PED-2026-118"
                  },
                  "public_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000,
                    "description": "Observaciones visibles para el cliente",
                    "example": "Revisar también el ruido de la suspensión."
                  },
                  "lines": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "object",
                      "properties": {
                        "description": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2000,
                          "description": "Concepto visible para el cliente",
                          "example": "Cambio de aceite y filtro"
                        },
                        "quantity": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100000,
                          "description": "Cantidad (horas en mano de obra)",
                          "example": 1
                        },
                        "unit_price": {
                          "type": "number",
                          "minimum": -1000000,
                          "maximum": 1000000,
                          "description": "Precio unitario de venta sin impuestos, en euros",
                          "example": 65
                        },
                        "tax_rate": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "minimum": 0,
                          "maximum": 30,
                          "description": "Tipo impositivo; si se omite, el del taller (IVA/IGIC/IPSI según su zona)",
                          "example": 21
                        },
                        "discount_pct": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "description": "Descuento en %",
                          "example": 0
                        },
                        "line_type": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "enum": [
                            "labor",
                            "diagnosis",
                            "materials",
                            "parts",
                            "pieces",
                            "storage",
                            "other",
                            null
                          ],
                          "example": "labor"
                        },
                        "reference": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "maxLength": 100,
                          "description": "Referencia de recambio",
                          "example": null
                        },
                        "group_title": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "maxLength": 200,
                          "description": "Título del grupo al que pertenece",
                          "example": null
                        }
                      },
                      "required": [
                        "description",
                        "quantity",
                        "unit_price"
                      ],
                      "additionalProperties": false,
                      "description": "Partida nueva"
                    },
                    "description": "Partidas iniciales; los totales los calcula el servidor"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Enviar al cliente el acuse con su enlace de seguimiento. Exige también communications:send",
                    "example": false
                  }
                },
                "required": [
                  "client_id"
                ],
                "additionalProperties": false,
                "description": "Presupuesto nuevo; entra en estado «Pendiente» con canal «API»"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetDetail"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_belongs_to_other_client: Esa matrícula ya está dada de alta a nombre de otro cliente. | client_erased: El cliente pidió el borrado de sus datos (RGPD): su ficha no admite cambios ni altas asociadas. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}": {
      "get": {
        "operationId": "getBudget",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Detalle de un presupuesto",
        "description": "Partidas (descripción, cantidad, precio unitario, impuesto, tipo, descuento), totales con desglose, cita, fechas de entrada y salida, responsable y URL pública de seguimiento.\n\nPermiso necesario: `budgets:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "x-client-key": "own:budget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetDetail"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/activity": {
      "get": {
        "operationId": "getBudgetActivity",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Historial del presupuesto",
        "description": "Lo que ha pasado en el presupuesto, de más reciente a más antiguo: cambios de estado, envíos, aceptación (con su origen), citas, mensajes del cliente, facturación… El mismo registro que ve el taller en la ficha; public=true marca lo que también ve el cliente. Los códigos de un solo uso y enlaces de acceso salen ocultos.\n\nPermiso necesario: `budgets:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "x-client-key": "own:budget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "visibility",
            "in": "query",
            "required": false,
            "description": "all: todo el registro; public: solo lo que ve el cliente.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "public"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultados por página (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opaco devuelto en next_cursor de la página anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "has_more"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ActivityEntry"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor opaco para pedir la página siguiente; null si no hay más",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si quedan más resultados",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/lines": {
      "post": {
        "operationId": "addBudgetLine",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Añadir una partida",
        "description": "Las demás partidas conservan su id. Deja instantánea previa y registro como cualquier edición del programa. 409 budget_locked si el presupuesto está Facturado, Facturado externamente, Cancelado, Rechazado, Desistido o ya tiene factura.\n\nPermiso necesario: `budgets:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000,
                    "description": "Concepto visible para el cliente",
                    "example": "Cambio de aceite y filtro"
                  },
                  "quantity": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "description": "Cantidad (horas en mano de obra)",
                    "example": 1
                  },
                  "unit_price": {
                    "type": "number",
                    "minimum": -1000000,
                    "maximum": 1000000,
                    "description": "Precio unitario de venta sin impuestos, en euros",
                    "example": 65
                  },
                  "tax_rate": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 30,
                    "description": "Tipo impositivo; si se omite, el del taller (IVA/IGIC/IPSI según su zona)",
                    "example": 21
                  },
                  "discount_pct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Descuento en %",
                    "example": 0
                  },
                  "line_type": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "labor",
                      "diagnosis",
                      "materials",
                      "parts",
                      "pieces",
                      "storage",
                      "other",
                      null
                    ],
                    "example": "labor"
                  },
                  "reference": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "description": "Referencia de recambio",
                    "example": null
                  },
                  "group_title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Título del grupo al que pertenece",
                    "example": null
                  }
                },
                "required": [
                  "description",
                  "quantity",
                  "unit_price"
                ],
                "additionalProperties": false,
                "description": "Partida nueva"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "line",
                    "budget"
                  ],
                  "additionalProperties": false,
                  "description": "Partida creada y presupuesto con los totales recalculados",
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/BudgetLine"
                    },
                    "budget": {
                      "$ref": "#/components/schemas/BudgetDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El presupuesto está cerrado y ya no admite este cambio. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/lines/{lineId}": {
      "patch": {
        "operationId": "updateBudgetLine",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Modificar una partida",
        "description": "Permiso necesario: `budgets:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "required": true,
            "description": "Id de la partida.",
            "schema": {
              "type": "integer",
              "example": 5501
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2000,
                    "description": "Concepto visible para el cliente",
                    "example": "Cambio de aceite y filtro"
                  },
                  "quantity": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "description": "Cantidad (horas en mano de obra)",
                    "example": 1
                  },
                  "unit_price": {
                    "type": "number",
                    "minimum": -1000000,
                    "maximum": 1000000,
                    "description": "Precio unitario de venta sin impuestos, en euros",
                    "example": 65
                  },
                  "tax_rate": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 30,
                    "description": "Tipo impositivo; si se omite, el del taller (IVA/IGIC/IPSI según su zona)",
                    "example": 21
                  },
                  "discount_pct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Descuento en %",
                    "example": 0
                  },
                  "line_type": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "labor",
                      "diagnosis",
                      "materials",
                      "parts",
                      "pieces",
                      "storage",
                      "other",
                      null
                    ],
                    "example": "labor"
                  },
                  "reference": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "description": "Referencia de recambio",
                    "example": null
                  },
                  "group_title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Título del grupo al que pertenece",
                    "example": null
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Cambios parciales de una partida"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "line",
                    "budget"
                  ],
                  "additionalProperties": false,
                  "description": "Partida modificada y presupuesto recalculado",
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/BudgetLine"
                    },
                    "budget": {
                      "$ref": "#/components/schemas/BudgetDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El presupuesto está cerrado y ya no admite este cambio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteBudgetLine",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Quitar una partida",
        "description": "Devuelve el presupuesto con los totales recalculados. La partida queda en la instantánea previa del historial de versiones.\n\nPermiso necesario: `budgets:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "required": true,
            "description": "Id de la partida.",
            "schema": {
              "type": "integer",
              "example": 5501
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetDetail"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El presupuesto está cerrado y ya no admite este cambio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/status": {
      "post": {
        "operationId": "changeBudgetStatus",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Cambiar el estado",
        "description": "Admite Pendiente/En cotización/Enviado (antes de la aceptación), En curso (presupuesto ya aceptado), Finalizado (desde Aprobado, En curso o En espera) y Cancelado. «Aprobado» responde 403 client_acceptance_required: la aceptación la firma el cliente desde el enlace que le envía el taller, o la registra tu integración en su nombre con POST /budgets/{id}/accept (permiso budgets:accept). Rechazar va por POST /budgets/{id}/reject y facturar por POST /budgets/{id}/invoice (permiso invoices:write); aquí responden 403 status_transition_forbidden, igual que desistir; desde Facturado o Cancelado, 409 budget_locked. «Enviado» exige además communications:send y sent_via: registra que TU sistema ya lo envió (no lo envía). Ningún cambio avisa al cliente salvo Finalizado con notify_client=true y communications:send.\n\nPermiso necesario: `budgets:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 40,
                    "description": "Pendiente, En cotización, Enviado, En curso, Finalizado o Cancelado",
                    "example": "En curso"
                  },
                  "sent_via": {
                    "type": "string",
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp"
                    ],
                    "description": "Solo con status=Enviado: canal por el que TU sistema ha enviado el presupuesto. Exige communications:send",
                    "example": "email"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Con Finalizado: avisar al cliente de que el vehículo está listo (según los avisos configurados). Exige communications:send",
                    "example": false
                  }
                },
                "required": [
                  "status"
                ],
                "additionalProperties": false,
                "description": "Cambio de estado"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetDetail"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente. | client_acceptance_required: Para aceptar un presupuesto usa POST /budgets/{id}/accept con el permiso budgets:accept y la evidencia de quién acepta. | status_transition_forbidden: Ese cambio de estado no está disponible por API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — invalid_status_transition: El presupuesto no puede pasar a ese estado desde el estado actual. | budget_locked: El presupuesto está cerrado y ya no admite este cambio. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/accept": {
      "post": {
        "operationId": "acceptBudget",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Aceptar en nombre del cliente",
        "description": "Para cuando el cliente acepta por otra vía (teléfono, en persona, tu web…). Obligatoria la evidencia: accepted_by y channel (phone, in_person, email, whatsapp, other); note es opcional. Pasa por la misma regla de aceptación del programa: estado Aprobado, transición, datos fiscales mínimos del cliente (si faltan, 422 fiscal_data_incomplete con missing), descuento de campaña, aviso interno al taller, confirmación al cliente según los avisos configurados y webhooks lead.accepted y budget.accepted. Solo desde «Enviado» (si ya está aceptado responde 200 con already=true); Cancelado, Rechazado, Desistido, Finalizado o Facturado responden 409 budget_locked. Si el taller exige firma, la API acepta igualmente y lo deja escrito: «sin firma, aceptado por integración» (signature=waived_by_integration). En el historial queda «Aceptado por API en nombre del cliente · integración <clave> · <accepted_by> por <canal>».\n\nPermiso necesario: `budgets:accept`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:accept",
        "x-client-key": "own:budget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "accepted_by": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "description": "Nombre de la persona que acepta (el propio cliente o quien le representa)",
                    "example": "Laura Gómez"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "phone",
                      "in_person",
                      "email",
                      "whatsapp",
                      "other"
                    ],
                    "description": "Canal por el que el cliente ha dado su aceptación",
                    "example": "phone"
                  },
                  "note": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "description": "Nota opcional (referencia de la llamada, del email…)",
                    "example": "Confirmado por teléfono a las 10:12"
                  }
                },
                "required": [
                  "accepted_by",
                  "channel"
                ],
                "additionalProperties": false,
                "description": "Evidencia de la aceptación en nombre del cliente"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetDecision"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El presupuesto está cerrado y ya no admite este cambio. | invalid_status_transition: El presupuesto no puede pasar a ese estado desde el estado actual. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields». | fiscal_data_incomplete: Faltan datos fiscales del cliente para poder facturar: revisa la lista «missing».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/reject": {
      "post": {
        "operationId": "rejectBudget",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Rechazar en nombre del cliente",
        "description": "Con motivo obligatorio. Misma regla que el rechazo del cliente: desde Pendiente, En cotización, Enviado o Aprobado (si el vehículo aún no ha entrado). Emite lead.rejected y budget.rejected y queda en el historial con el nombre de la clave.\n\nPermiso necesario: `budgets:accept`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:accept",
        "x-client-key": "own:budget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 400,
                    "description": "Motivo del rechazo",
                    "example": "Lo hará en otro taller"
                  },
                  "rejected_by": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 120,
                    "description": "Quién lo comunica",
                    "example": "Laura Gómez"
                  },
                  "channel": {
                    "type": "string",
                    "enum": [
                      "phone",
                      "in_person",
                      "email",
                      "whatsapp",
                      "other"
                    ],
                    "description": "Canal por el que el cliente lo ha comunicado",
                    "example": "phone"
                  }
                },
                "required": [
                  "reason"
                ],
                "additionalProperties": false,
                "description": "Rechazo en nombre del cliente"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetDecision"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El presupuesto está cerrado y ya no admite este cambio. | invalid_status_transition: El presupuesto no puede pasar a ese estado desde el estado actual. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/invoice": {
      "post": {
        "operationId": "invoiceBudget",
        "tags": [
          "Facturas"
        ],
        "summary": "Facturar un presupuesto",
        "description": "Usa la misma facturación del programa: copia las partidas y el impuesto del presupuesto, crea el asiento contable y, con issue=true (por defecto), emite la factura con la serie que toque, número correlativo, hash encadenado y QR Verifactu. El presupuesto pasa a Facturado. Se factura uno Aprobado, En curso, En espera o Finalizado con importe; si ya tiene factura se devuelve esa (created=false), nunca una segunda. Emite invoice.created e invoice.issued. Las facturas rectificativas no están disponibles por API: se hacen desde el programa.\n\nPermiso necesario: `invoices:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "issue": {
                    "type": "boolean",
                    "description": "true (por defecto): emite la factura con número definitivo, serie, hash Verifactu y asiento contable. false: la deja en borrador para revisarla en el programa",
                    "example": true
                  },
                  "pay_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 40,
                    "description": "Forma de pago que figura en la factura (por defecto CAJA)",
                    "example": "TARJETA"
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Factura de un presupuesto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceCreated"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_not_invoiceable: El presupuesto no se puede facturar en su estado actual. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields». | invoice_rejected: El programa no ha podido emitir la factura: revisa el mensaje.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/documents": {
      "post": {
        "operationId": "uploadBudgetDocument",
        "tags": [
          "Presupuestos"
        ],
        "summary": "Adjuntar un documento",
        "description": "multipart/form-data con el campo «file» (JPEG, PNG, WebP o PDF, comprobado por su contenido; máximo 4 MB). Por defecto solo lo ve el taller; client_visible=true lo enseña en el enlace de seguimiento y mechanic_visible=true en la app del mecánico. La huella de idempotencia incluye el fichero.\n\nPermiso necesario: `budgets:write`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "x-client-key": "own:budget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del presupuesto.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "additionalProperties": false,
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "Fichero"
                  },
                  "client_visible": {
                    "type": "string",
                    "enum": [
                      "true",
                      "false"
                    ],
                    "example": "false"
                  },
                  "mechanic_visible": {
                    "type": "string",
                    "enum": [
                      "true",
                      "false"
                    ],
                    "example": "false"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BudgetDocument"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Error — payload_too_large: El fichero supera el tamaño máximo (4 MB).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Error — unsupported_media_type: Tipo de fichero no admitido: solo JPEG, PNG, WebP o PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices": {
      "get": {
        "operationId": "listInvoices",
        "tags": [
          "Facturas"
        ],
        "summary": "Listar facturas",
        "description": "Facturas emitidas y borradores, ordenadas por id. Incluye el resumen de cobros.\n\nPermiso necesario: `invoices:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Fecha de factura desde.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Fecha de factura hasta.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filtrar por cliente.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Estado de la factura.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "issued",
                "cancelled"
              ]
            }
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "description": "Serie exacta.",
            "schema": {
              "type": "string",
              "example": "F26"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultados por página (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opaco devuelto en next_cursor de la página anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "has_more"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invoice"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor opaco para pedir la página siguiente; null si no hay más",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si quedan más resultados",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}": {
      "get": {
        "operationId": "getInvoice",
        "tags": [
          "Facturas"
        ],
        "summary": "Detalle de una factura",
        "description": "Permiso necesario: `invoices:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "x-client-key": "own:invoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la factura.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceDetail"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/pdf": {
      "get": {
        "operationId": "getInvoicePdf",
        "tags": [
          "Facturas"
        ],
        "summary": "PDF de una factura",
        "description": "El mismo PDF que genera el programa (con QR Verifactu si la factura está emitida). Respuesta application/pdf.\n\nPermiso necesario: `invoices:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "x-client-key": "own:invoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la factura.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/payments": {
      "post": {
        "operationId": "addInvoicePayment",
        "tags": [
          "Facturas"
        ],
        "summary": "Anotar un cobro",
        "description": "Mismo registro de cobros que la ficha de la factura: la marca «pagada» se recalcula con todos los cobros (nunca se fuerza) y se emite payment.received. Solo sobre facturas emitidas. Devuelve la factura con su resumen de cobros.\n\nPermiso necesario: `invoices:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la factura.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "number",
                    "minimum": 0.01,
                    "maximum": 1000000,
                    "description": "Importe cobrado en euros",
                    "example": 245.3
                  },
                  "date": {
                    "type": "string",
                    "format": "date",
                    "description": "Fecha del cobro (por defecto hoy)",
                    "example": "2026-10-13"
                  },
                  "pay_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 40,
                    "description": "Forma de pago",
                    "example": "TARJETA"
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 500,
                    "example": "Cobrado en mostrador"
                  }
                },
                "required": [
                  "amount"
                ],
                "additionalProperties": false,
                "description": "Cobro de una factura"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceDetail"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields». | invoice_rejected: El programa no ha podido emitir la factura: revisa el mensaje.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments": {
      "get": {
        "operationId": "listAppointments",
        "tags": [
          "Citas"
        ],
        "summary": "Citas confirmadas y propuestas",
        "description": "Por defecto los próximos 30 días. status=confirmed son citas fijadas en la agenda; status=proposed son propuestas del cliente pendientes de que el taller confirme (bloquean el hueco).\n\nPermiso necesario: `appointments:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inicio del rango (por defecto ahora).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Fin del rango (por defecto +30 días, máximo 1 año).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Solo un tipo.",
            "schema": {
              "type": "string",
              "enum": [
                "confirmed",
                "proposed"
              ]
            }
          },
          {
            "name": "box_id",
            "in": "query",
            "required": false,
            "description": "Filtrar por elevador/box.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "from",
                    "to"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Appointment"
                      }
                    },
                    "from": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-10-06T00:00:00.000Z"
                    },
                    "to": {
                      "type": "string",
                      "format": "date-time",
                      "example": "2026-11-05T00:00:00.000Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAppointment",
        "tags": [
          "Citas"
        ],
        "summary": "Proponer o reservar una cita",
        "description": "Sigue el modo de reserva del taller (booking_mode en GET /workshop). En «propose» se crea una propuesta (status=proposed) que bloquea el hueco hasta que el taller la confirma; pedir mode=book responde 403 booking_mode_propose_only. En «book» la cita queda en firme (status=confirmed), salvo que pidas mode=propose. El hueco se valida con la misma lógica que /appointments/availability; si no está libre, 409 slot_unavailable con hasta 3 alternativas en error.alternatives. No se avisa al cliente salvo notify_client=true con communications:send.\n\nPermiso necesario: `appointments:write`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "budget_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Presupuesto al que pertenece la cita",
                    "example": 1234
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Inicio con zona horaria (ISO 8601)",
                    "example": "2026-10-14T09:00:00+02:00"
                  },
                  "box_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Elevador/box; si se omite, el primero libre",
                    "example": 2
                  },
                  "duration_minutes": {
                    "type": "integer",
                    "minimum": 15,
                    "maximum": 720,
                    "description": "Duración; por defecto la mínima de la agenda",
                    "example": 60
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "propose",
                      "book"
                    ],
                    "description": "propose: propuesta que el taller confirma. book: cita en firme (solo si el taller trabaja en modo «reservar»). Por defecto, el modo del taller",
                    "example": "propose"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Enviar la confirmación de cita al cliente (solo citas en firme). Exige communications:send",
                    "example": false
                  }
                },
                "required": [
                  "budget_id",
                  "start"
                ],
                "additionalProperties": false,
                "description": "Cita o propuesta de cita"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | booking_mode_propose_only: El taller trabaja en modo «proponer cita»: solo se pueden crear propuestas que el taller confirma.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — slot_unavailable: Ese hueco no está disponible. | budget_locked: El presupuesto está cerrado y ya no admite este cambio. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments/availability": {
      "get": {
        "operationId": "getAvailability",
        "tags": [
          "Citas"
        ],
        "summary": "Huecos libres",
        "description": "Misma lógica que la agenda y la web de seguimiento: horario del taller y de cada box, comida, festivos nacionales, autonómicos y locales, citas abiertas y propuestas pendientes (que bloquean su hueco). Inicios cada 30 min y al menos 1 h desde ahora. Por defecto los próximos 7 días (máximo 31). Incluye booking_mode del taller.\n\nPermiso necesario: `appointments:read`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "x-client-key": "open",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Desde (por defecto ahora).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Hasta (por defecto +7 días; máximo 31 días).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "duration_minutes",
            "in": "query",
            "required": false,
            "description": "Duración de la cita (15–720). Por defecto la mínima de la agenda.",
            "schema": {
              "type": "integer",
              "example": 60
            }
          },
          {
            "name": "box_id",
            "in": "query",
            "required": false,
            "description": "Solo ese box.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Máximo de huecos (1–500).",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentAvailability"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments/{id}": {
      "delete": {
        "operationId": "cancelAppointment",
        "tags": [
          "Citas"
        ],
        "summary": "Anular una cita o retirar una propuesta",
        "description": "b<presupuesto>: anula la cita confirmada (igual que «Cancelar cita» en la ficha). p<propuesta>: retira la propuesta pendiente; con notify_client=true y communications:send se invita al cliente a elegir otra hora. Emite appointment.cancelled.\n\nPermiso necesario: `appointments:write`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "x-client-key": "own:appointment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la cita (b1234 o p88).",
            "schema": {
              "type": "string",
              "example": "b1234"
            }
          },
          {
            "name": "notify_client",
            "in": "query",
            "required": false,
            "description": "Solo propuestas: avisar al cliente.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentCancelled"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getAppointment",
        "tags": [
          "Citas"
        ],
        "summary": "Detalle de una cita",
        "description": "Permiso necesario: `appointments:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "x-client-key": "own:appointment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la cita (b1234 o p88).",
            "schema": {
              "type": "string",
              "example": "b1234"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateAppointment",
        "tags": [
          "Citas"
        ],
        "summary": "Mover una cita o cambiar una propuesta",
        "description": "b<presupuesto>: mueve la cita en firme a la nueva hora (solo si el taller trabaja en modo «reservar»; en modo «proponer» responde 403 booking_mode_propose_only: crea una propuesta con POST /appointments). p<propuesta>: retira la propuesta y crea otra a la nueva hora (cambia el id). El hueco se valida igual que al crear (409 slot_unavailable con alternativas). Emite appointment.updated.\n\nPermiso necesario: `appointments:write`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "x-client-key": "own:appointment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id de la cita (b1234 o p88).",
            "schema": {
              "type": "string",
              "example": "b1234"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Nuevo inicio con zona horaria (ISO 8601)",
                    "example": "2026-10-15T10:00:00+02:00"
                  },
                  "box_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Elevador/box; si se omite, el primero libre",
                    "example": 2
                  },
                  "duration_minutes": {
                    "type": "integer",
                    "minimum": 15,
                    "maximum": 720,
                    "description": "Duración; por defecto la mínima de la agenda",
                    "example": 60
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Avisar al cliente de la nueva hora (solo citas en firme). Exige communications:send",
                    "example": false
                  }
                },
                "required": [
                  "start"
                ],
                "additionalProperties": false,
                "description": "Mover una cita o cambiar una propuesta"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Appointment"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | booking_mode_propose_only: El taller trabaja en modo «proponer cita»: solo se pueden crear propuestas que el taller confirma.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — slot_unavailable: Ese hueco no está disponible. | budget_locked: El presupuesto está cerrado y ya no admite este cambio.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/catalog/services": {
      "get": {
        "operationId": "listServices",
        "tags": [
          "Catálogo"
        ],
        "summary": "Categorías y subcategorías de servicio",
        "description": "Permiso necesario: `catalog:read`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "x-client-key": "open",
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ServiceCategory"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/catalog/rates": {
      "get": {
        "operationId": "listRates",
        "tags": [
          "Catálogo"
        ],
        "summary": "Tarifas de mano de obra",
        "description": "Solo el precio de venta por hora; el coste nunca sale por la API.\n\nPermiso necesario: `catalog:read`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "x-client-key": "open",
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LaborRate"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stock": {
      "get": {
        "operationId": "listStock",
        "tags": [
          "Recambios"
        ],
        "summary": "Listar recambios del stock",
        "description": "Existencias con unidad y decimales (quantity, reserved, pending, planned, available, min_stock), referencias y precio de venta. Disponible = cantidad − bloqueado por presupuestos aceptados o en curso; lo previsto en presupuestos abiertos no bloquea. El coste de compra, el proveedor y el historial de compras nunca salen por la API. Por defecto solo piezas activas.\n\nPermiso necesario: `stock:read`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "stock:read",
        "x-client-key": "open",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Busca en nombre, marca, referencias y EAN (las referencias sin tener en cuenta espacios, puntos ni guiones).",
            "schema": {
              "type": "string",
              "example": "turbo"
            }
          },
          {
            "name": "reference",
            "in": "query",
            "required": false,
            "description": "Referencia o EAN exactos, sin tener en cuenta espacios, puntos ni guiones.",
            "schema": {
              "type": "string",
              "example": "A6540907801"
            }
          },
          {
            "name": "in_stock",
            "in": "query",
            "required": false,
            "description": "true: solo con cantidad en el almacén; false: solo con cantidad cero (mira available para lo libre).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "include_inactive",
            "in": "query",
            "required": false,
            "description": "Incluir piezas dadas de baja del inventario.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "description": "Solo registros modificados a partir de esta fecha (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01T00:00:00Z"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultados por página (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opaco devuelto en next_cursor de la página anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "has_more"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/StockItem"
                      }
                    },
                    "next_cursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Cursor opaco para pedir la página siguiente; null si no hay más",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si quedan más resultados",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/stock/{id}": {
      "get": {
        "operationId": "getStockItem",
        "tags": [
          "Recambios"
        ],
        "summary": "Detalle de un recambio",
        "description": "Permiso necesario: `stock:read`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "stock:read",
        "x-client-key": "open",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del recambio.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StockItem"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/communications": {
      "get": {
        "operationId": "listCommunications",
        "tags": [
          "Comunicaciones"
        ],
        "summary": "Registro de comunicaciones",
        "description": "Últimas comunicaciones (email, SMS, WhatsApp, llamadas, push) con su contenido completo, de más reciente a más antigua.\n\nPermiso necesario: `communications:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "Canal.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "email",
                "sms",
                "whatsapp",
                "call",
                "push"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Máximo de resultados (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "has_more"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Communication"
                      }
                    },
                    "next_cursor": {
                      "type": "null"
                    },
                    "has_more": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/logs": {
      "get": {
        "operationId": "listLogs",
        "tags": [
          "Comunicaciones"
        ],
        "summary": "Registro de comunicaciones (alias antiguo)",
        "description": "Misma consulta que /communications pero devuelve { items }. Se mantiene por compatibilidad; usa /communications.\n\nPermiso necesario: `communications:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "Canal.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "email",
                "sms",
                "whatsapp",
                "call",
                "push"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Máximo de resultados (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Communication"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/email": {
      "post": {
        "operationId": "sendEmail",
        "tags": [
          "Comunicaciones"
        ],
        "summary": "Enviar un email transaccional",
        "description": "Solo texto: sale con la plantilla, el nombre y la cuenta de correo del taller, y queda en el registro de Comunicación. No admite HTML ni remitente propio, y replyTo solo puede ser una dirección del dominio del taller. Si rebota, el email del cliente se marca como no válido.\n\nPermiso necesario: `communications:send`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:send",
        "x-client-key": "deny",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "subject",
                  "text"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "format": "email",
                    "example": "cliente@ejemplo.com"
                  },
                  "subject": {
                    "type": "string",
                    "maxLength": 200,
                    "example": "Su vehículo está listo"
                  },
                  "text": {
                    "type": "string",
                    "maxLength": 20000,
                    "example": "Puede pasar a recogerlo."
                  },
                  "replyTo": {
                    "type": "string",
                    "format": "email",
                    "example": "taller@ejemplo.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "example": {
                    "ok": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sms": {
      "post": {
        "operationId": "sendSms",
        "tags": [
          "Comunicaciones"
        ],
        "summary": "Enviar un SMS transaccional",
        "description": "Sale con el servicio de SMS configurado en el taller y queda en el registro de Comunicación, donde se actualiza su estado de entrega.\n\nPermiso necesario: `communications:send`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:send",
        "x-client-key": "deny",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "body"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "example": "+34600111222"
                  },
                  "body": {
                    "type": "string",
                    "example": "Su vehículo está listo para recoger."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "example": {
                    "ok": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/conversations": {
      "get": {
        "operationId": "listConversations",
        "tags": [
          "Conversaciones"
        ],
        "summary": "Conversaciones de la Bandeja",
        "description": "WhatsApp, SMS, chat de la web y email en una lista, como la Bandeja del programa, de la más reciente a la más antigua. Cada una con su clave estable (canal:número o canal:id), cliente, presupuesto activo, estado (open: falta contestar; pending: esperando al cliente; closed) y si la ventana de 24 h de WhatsApp sigue abierta.\n\nPermiso necesario: `communications:read`.\n\nClave de cliente: solo devuelve o crea datos de su cliente.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "x-client-key": "filtered",
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "Solo un canal.",
            "schema": {
              "type": "string",
              "enum": [
                "whatsapp",
                "sms",
                "chat",
                "email"
              ]
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Solo un estado.",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "pending",
                "closed"
              ]
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Actividad de los últimos N días (1–365).",
            "schema": {
              "type": "integer",
              "default": 30
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Máximo de resultados (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Conversation"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/conversations/{key}/messages": {
      "get": {
        "operationId": "listConversationMessages",
        "tags": [
          "Conversaciones"
        ],
        "summary": "Mensajes de una conversación",
        "description": "El hilo completo en orden cronológico, con autor (cliente, ia, equipo, automatico o integracion), texto y adjuntos. Los emails se devuelven como texto, sin la plantilla ni el hilo citado.\n\nPermiso necesario: `communications:read`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "x-client-key": "own:conversation",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Clave de la conversación: whatsapp:<9 dígitos>, sms:<9 dígitos>, chat:<id> o email:<id>.",
            "schema": {
              "type": "string",
              "example": "whatsapp:600111222"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "conversation",
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "conversation": {
                      "$ref": "#/components/schemas/Conversation"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ConversationMessage"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — conversation_not_found: No existe esa conversación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "sendConversationMessage",
        "tags": [
          "Conversaciones"
        ],
        "summary": "Responder en una conversación",
        "description": "Sale por el mismo canal y con las mismas funciones que la Bandeja del programa. WhatsApp: text y/o media_url dentro de la ventana de 24 h; fuera de ella solo con template_id (plantilla aprobada; si no, 422 whatsapp_window_closed). SMS: text (un fragmento). Chat web: text. Email: subject y text. El mensaje queda registrado con la integración como autor («API · <clave>»), marca la conversación como respondida y, si había un mensaje esperando al asistente externo, cancela la respuesta de respaldo del asistente del programa. En el entorno de pruebas nada sale de verdad (simulated=true).\n\nPermiso necesario: `messages:send`.\n\nClave de cliente: solo si el recurso es de su cliente; si no, 404.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "messages:send",
        "x-client-key": "own:conversation",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Clave de la conversación: whatsapp:<9 dígitos>, sms:<9 dígitos>, chat:<id> o email:<id>.",
            "schema": {
              "type": "string",
              "example": "whatsapp:600111222"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 4000,
                    "description": "Texto del mensaje. WhatsApp: máximo 1.600 caracteres; SMS: un fragmento, como en el programa",
                    "example": "Su coche estará listo a las 18:00."
                  },
                  "media_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 1000,
                    "description": "Solo WhatsApp: URL https pública de una imagen o PDF",
                    "example": null,
                    "pattern": "^https://"
                  },
                  "template_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Solo WhatsApp: plantilla aprobada (GET /messages/templates). Obligatoria fuera de la ventana de 24 h",
                    "example": 12
                  },
                  "template_variables": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "maxLength": 500
                    },
                    "description": "Valores de las etiquetas de la plantilla, p. ej. {\"#nombre#\": \"Laura\"}",
                    "example": {
                      "#nombre#": "Laura"
                    }
                  },
                  "subject": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Solo email: asunto (obligatorio en email)",
                    "example": "Su presupuesto"
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Respuesta en una conversación"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageSent"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — conversation_not_found: No existe esa conversación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — channel_unavailable: Ese canal no está configurado en el taller. | idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields». | whatsapp_window_closed: Han pasado más de 24 h desde el último mensaje del cliente: por WhatsApp solo se le puede escribir con una plantilla aprobada (template_id).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Error — upstream_error: Un servicio externo ha rechazado la operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/templates": {
      "get": {
        "operationId": "listWhatsAppTemplates",
        "tags": [
          "Conversaciones"
        ],
        "summary": "Plantillas de WhatsApp aprobadas",
        "description": "Las plantillas que Meta ha aprobado al taller: su id es el template_id para escribir fuera de la ventana de 24 h.\n\nPermiso necesario: `communications:read`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "x-client-key": "open",
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WhatsAppTemplate"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/settings/assistant": {
      "get": {
        "operationId": "getAssistantSettings",
        "tags": [
          "Ajustes"
        ],
        "summary": "Modo asistente externo",
        "description": "En qué canales contesta tu asistente en lugar del asistente del programa, la clave que los atiende, los minutos de respaldo y los canales que de verdad atiendes ahora (clave viva y webhook message.received activo).\n\nPermiso necesario: `settings:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "settings:write",
        "x-client-key": "deny",
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantSettings"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateAssistantSettings",
        "tags": [
          "Ajustes"
        ],
        "summary": "Cambiar el modo asistente externo",
        "description": "En los canales de external_channels el asistente del programa deja de contestar: cada mensaje entra, se registra, se emite message.received y se espera tu respuesta (POST /conversations/{key}/messages). Salvaguarda: si no respondes en fallback_minutes (10 por defecto), si la clave se revoca o caduca, o si no hay webhook message.received activo o su entrega falla, contesta el asistente del programa a ese mensaje y queda registrado. La clave que hace la petición pasa a ser la que atiende. external_channels=[] lo devuelve todo al programa.\n\nPermiso necesario: `settings:write`.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "settings:write",
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "external_channels": {
                    "type": "array",
                    "maxItems": 4,
                    "items": {
                      "type": "string",
                      "enum": [
                        "whatsapp",
                        "sms",
                        "chat",
                        "email"
                      ]
                    },
                    "description": "Canales en los que contesta tu asistente; [] devuelve todos al asistente del programa",
                    "example": [
                      "whatsapp"
                    ]
                  },
                  "fallback_minutes": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 120,
                    "description": "Si tu integración no responde en estos minutos, contesta el asistente del programa",
                    "example": 10
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Modo asistente externo (la clave que lo cambia queda como la que atiende)"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssistantSettings"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sandbox/keys": {
      "post": {
        "operationId": "createSandboxKey",
        "tags": [
          "Entorno de pruebas"
        ],
        "summary": "Pedir una clave de pruebas",
        "description": "Solo en https://demo.programa-taller.es (en las demás instancias responde 404 sandbox_only). Sin autenticación ni coste: devuelve una clave pt_test_… con todos los permisos, válida 30 días, que solo se muestra una vez. Máximo 3 al día por IP y por email. Los datos de la demo son ficticios y se restauran cada 2 horas; las claves, los webhooks y el registro de peticiones se conservan. Nada sale de verdad: los envíos se registran como simulados y los webhooks se emiten igual.\n\nSin autenticación.",
        "security": [],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 254,
                    "description": "Email de contacto del integrador",
                    "example": "dev@ejemplo.com",
                    "format": "email"
                  },
                  "company": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "example": "Integraciones Ejemplo SL"
                  },
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120,
                    "example": "Ana Pérez"
                  }
                },
                "required": [
                  "email",
                  "company",
                  "name"
                ],
                "additionalProperties": false,
                "description": "Alta de una clave de pruebas"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SandboxKey"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — sandbox_only: Solo disponible en el entorno de pruebas (https://demo.programa-taller.es).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sandbox/simulate/message": {
      "post": {
        "operationId": "simulateInboundMessage",
        "tags": [
          "Entorno de pruebas"
        ],
        "summary": "Simular un mensaje entrante",
        "description": "Solo en el entorno de pruebas. Crea un mensaje entrante ficticio por WhatsApp, SMS, chat web o email, como si lo hubiera escrito un cliente: entra en la Bandeja, se enlaza con el cliente si el teléfono o el email coinciden y dispara message.received para probar tu bot de principio a fin. Contesta con POST /conversations/{key}/messages (la respuesta se simula).\n\nCualquier clave válida, sin permiso concreto.\n\nClave de cliente: no disponible (403 client_key_forbidden).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-client-key": "deny",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clave única por operación (UUID recomendado). Repetirla con el mismo cuerpo en 24 h devuelve la respuesta guardada con Idempotent-Replay: true; con otro cuerpo, 409 idempotency_conflict.",
            "schema": {
              "type": "string",
              "minLength": 8,
              "maxLength": 255,
              "example": "5f0c7a52-3d1e-4b8a-9c61-2f7e1b0d4a93"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel": {
                    "type": "string",
                    "enum": [
                      "whatsapp",
                      "sms",
                      "chat",
                      "email"
                    ],
                    "example": "whatsapp"
                  },
                  "from": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 254,
                    "description": "Teléfono (whatsapp/sms), email (email) o nombre del visitante (chat)",
                    "example": "+34600111222"
                  },
                  "text": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4000,
                    "description": "Texto del mensaje",
                    "example": "Hola, ¿tienen hueco mañana para cambiar el aceite?"
                  },
                  "subject": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Solo email",
                    "example": "Consulta"
                  },
                  "media_urls": {
                    "type": "array",
                    "maxItems": 5,
                    "items": {
                      "type": "string",
                      "pattern": "^https://",
                      "maxLength": 1000
                    },
                    "description": "Adjuntos (URLs https)",
                    "example": []
                  }
                },
                "required": [
                  "channel",
                  "from",
                  "text"
                ],
                "additionalProperties": false,
                "description": "Mensaje entrante ficticio (solo entorno de pruebas)"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "key"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    },
                    "key": {
                      "type": "string",
                      "example": "whatsapp:600111222"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida. | idempotency_key_required: Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | client_key_forbidden: Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — sandbox_only: Solo disponible en el entorno de pruebas (https://demo.programa-taller.es).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — idempotency_conflict: Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta. | idempotency_in_progress: Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cuerpo de la petición no es válido: revisa la lista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "Listar los webhooks de la clave",
        "description": "Incluye el catálogo de eventos disponibles. Nunca devuelve los secretos.\n\nPermiso necesario: `webhooks:manage`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "x-client-key": "open",
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "events"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Webhook"
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "event": {
                            "type": "string",
                            "example": "lead.accepted"
                          },
                          "label": {
                            "type": "string",
                            "example": "Presupuesto aceptado"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Crear un webhook",
        "description": "El secreto de firma (whsec_…) solo viaja en esta respuesta. Con una clave de cliente solo se entregan los eventos de ese cliente (los que no pertenecen a ningún cliente, como campaign.finished, no llegan).\n\nPermiso necesario: `webhooks:manage`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "x-client-key": "open",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "events"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "example": "https://tu-sistema.com/webhooks/taller"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "example": "lead.accepted"
                    }
                  },
                  "description": {
                    "type": "string",
                    "example": "CRM"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "item",
                    "secret"
                  ],
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    "secret": {
                      "type": "string",
                      "example": "whsec_…"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "get": {
        "operationId": "getWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Detalle de un webhook y sus últimas entregas",
        "description": "Permiso necesario: `webhooks:manage`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "x-client-key": "open",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del webhook.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/Webhook"
                    },
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Modificar un webhook",
        "description": "Permiso necesario: `webhooks:manage`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "x-client-key": "open",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del webhook.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "description": {
                    "type": "string"
                  },
                  "active": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Borrar un webhook",
        "description": "Permiso necesario: `webhooks:manage`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "x-client-key": "open",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del webhook.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/test": {
      "post": {
        "operationId": "testWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Enviar una entrega de prueba (test.ping) al webhook",
        "description": "Permiso necesario: `webhooks:manage`.\n\nClave de cliente: permitido (no lleva datos de otros clientes).",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "x-client-key": "open",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del webhook.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Respuesta correcta",
            "headers": {
              "X-RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "epoch (s)"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "example": {
                    "ok": true,
                    "status": 200
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petición no es válida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clave API no es válida para esta instancia.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: La dirección IP de origen no está en la lista permitida de esta clave. | insufficient_scope: La clave API no tiene el permiso necesario para esta operación. | plan_scope_not_allowed: El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan. | test_key_forbidden: Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No se ha encontrado el recurso.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superado el límite de peticiones de esta clave. Espera y reintenta. | plan_quota_exceeded: Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error interno.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "lead.created": {
      "post": {
        "summary": "Presupuesto creado",
        "description": "Se ha creado un presupuesto nuevo (desde el programa, la web, el email o el asistente).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Presupuestos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "lead.created"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "lead.sent": {
      "post": {
        "summary": "Presupuesto enviado",
        "description": "El presupuesto se ha enviado al cliente.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Presupuestos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "lead.sent"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "lead.accepted": {
      "post": {
        "summary": "Presupuesto aceptado",
        "description": "El cliente o el taller han aprobado el presupuesto.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Presupuestos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "lead.accepted"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "lead.rejected": {
      "post": {
        "summary": "Presupuesto rechazado",
        "description": "El presupuesto se ha rechazado, con su motivo si lo hay.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Presupuestos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "lead.rejected"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "lead.status_changed": {
      "post": {
        "summary": "Cambio de estado",
        "description": "Cualquier cambio de estado del presupuesto (incluye los anteriores).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Presupuestos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "lead.status_changed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "budget.accepted": {
      "post": {
        "summary": "Presupuesto aceptado (budget)",
        "description": "Igual que lead.accepted, con el nombre nuevo: el presupuesto ha pasado a Aprobado (cliente, taller o integración con budgets:accept).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Presupuestos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "budget.accepted"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "budget.rejected": {
      "post": {
        "summary": "Presupuesto rechazado (budget)",
        "description": "Igual que lead.rejected, con el nombre nuevo: el presupuesto ha pasado a Rechazado, con su motivo.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Presupuestos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "budget.rejected"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "appointment.proposed": {
      "post": {
        "summary": "Cita propuesta",
        "description": "Un cliente propone una cita pendiente de confirmar por el taller.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Citas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "appointment.proposed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "appointment.confirmed": {
      "post": {
        "summary": "Cita confirmada",
        "description": "Una cita queda confirmada en la agenda.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Citas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "appointment.confirmed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "appointment.cancelled": {
      "post": {
        "summary": "Cita cancelada",
        "description": "Se ha anulado la cita de un presupuesto.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Citas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "appointment.cancelled"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "appointment.created": {
      "post": {
        "summary": "Cita creada",
        "description": "Un presupuesto que no tenía cita queda citado en la agenda (programa, web de seguimiento, asistente o API).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Citas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "appointment.created"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "appointment.updated": {
      "post": {
        "summary": "Cita cambiada",
        "description": "Se ha movido la fecha, la hora o el box de una cita existente, o se ha cambiado una propuesta por API.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Citas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "appointment.updated"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.created": {
      "post": {
        "summary": "Vehículo creado",
        "description": "Se ha dado de alta un vehículo.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.created"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.updated": {
      "post": {
        "summary": "Vehículo actualizado",
        "description": "Se han modificado los datos o el estado de un vehículo.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.updated"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.checked_in": {
      "post": {
        "summary": "Vehículo recibido",
        "description": "El vehículo ha entrado en el taller (recepción o sin cita).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.checked_in"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.arriving": {
      "post": {
        "summary": "Cliente de camino",
        "description": "El cliente avisa desde su enlace de entrada de que va de camino, llegará tarde o ya está en el taller, con la hora estimada.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.arriving"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.ready": {
      "post": {
        "summary": "Vehículo listo",
        "description": "La reparación ha terminado y el vehículo está listo para recoger.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.ready"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.arrived": {
      "post": {
        "summary": "Cliente en recepción",
        "description": "El personal marca en la cola de recepción que el cliente ha llegado al mostrador (la entrada por kiosco o móvil emite vehicle.checked_in).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.arrived"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.eta_updated": {
      "post": {
        "summary": "Hora prevista de entrega",
        "description": "Se ha fijado o cambiado la hora prevista de entrega del vehículo; indica si se ha comunicado al cliente.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.eta_updated"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.delivered": {
      "post": {
        "summary": "Vehículo entregado",
        "description": "El cliente ha retirado el vehículo.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.delivered"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "vehicle.pickup_planned": {
      "post": {
        "summary": "Recogida planificada",
        "description": "El cliente ha elegido el día y la franja en que pasará a recoger el vehículo (date, slot: morning/afternoon/any).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "vehicle.pickup_planned"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "courtesy_car.loaned": {
      "post": {
        "summary": "Coche de cortesía prestado",
        "description": "Se ha entregado un coche de cortesía al cliente con contrato firmado (matrícula, km y combustible de salida, devolución prevista).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "courtesy_car.loaned"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "courtesy_car.returned": {
      "post": {
        "summary": "Coche de cortesía devuelto",
        "description": "El cliente ha devuelto el coche de cortesía (km y combustible de entrada, días y km extra calculados).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Vehículos"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "courtesy_car.returned"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "stock.low": {
      "post": {
        "summary": "Stock bajo",
        "description": "Lo disponible de un recambio ha bajado de su stock mínimo (partId, name, unit, quantity, available, reserved, planned, minStock). Se envía una vez por cambio de nivel, no cada día.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Recambios"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "stock.low"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "stock.out": {
      "post": {
        "summary": "Recambio agotado",
        "description": "No queda nada disponible de un recambio con mínimo o con consumo (mismos campos que stock.low). Una vez por cambio de nivel.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Recambios"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "stock.out"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "stock.shortage": {
      "post": {
        "summary": "Falta stock en un presupuesto aceptado",
        "description": "Un presupuesto ha pasado a Aprobado o En curso y no había stock suficiente: leadId e items [{ partId, missing }] con lo que queda pendiente de pedir.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Recambios"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "stock.shortage"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "invoice.created": {
      "post": {
        "summary": "Factura creada",
        "description": "Se ha creado una factura (aún en borrador, sin número definitivo), por ejemplo al facturar un presupuesto.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Facturas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "invoice.created"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "invoice.issued": {
      "post": {
        "summary": "Factura emitida",
        "description": "Se ha emitido una factura con número definitivo.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Facturas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "invoice.issued"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "payment.received": {
      "post": {
        "summary": "Cobro registrado",
        "description": "Se ha anotado un cobro sobre una factura.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Facturas"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "payment.received"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "client.created": {
      "post": {
        "summary": "Cliente creado",
        "description": "Se ha dado de alta un cliente.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Clientes"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "client.created"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "client.updated": {
      "post": {
        "summary": "Cliente actualizado",
        "description": "Se han modificado los datos de un cliente.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Clientes"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "client.updated"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "communication.inbound": {
      "post": {
        "summary": "Mensaje entrante",
        "description": "Ha llegado un email, SMS, WhatsApp o llamada de un cliente.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "communication.inbound"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "message.received": {
      "post": {
        "summary": "Mensaje recibido en una conversación",
        "description": "Un cliente ha escrito por WhatsApp, SMS, chat web o email: conversación (clave estable canal:número), cliente, presupuesto activo, texto, adjuntos y fecha, y si el asistente externo debe contestar (external_bot).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "message.received"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "call.completed": {
      "post": {
        "summary": "Llamada analizada",
        "description": "Una llamada (del equipo o del asistente de voz) ha terminado y se ha analizado: resumen, motivo, sentimiento y transcripción si existe.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "call.completed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "email.sent": {
      "post": {
        "summary": "Email enviado",
        "description": "Se ha enviado un email (campañas, avisos o API).\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "email.sent"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "email.failed": {
      "post": {
        "summary": "Email fallido",
        "description": "Un email no se ha podido enviar.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "email.failed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "email.opened": {
      "post": {
        "summary": "Email abierto",
        "description": "El destinatario ha abierto el email.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "email.opened"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "email.clicked": {
      "post": {
        "summary": "Clic en email",
        "description": "El destinatario ha pulsado un enlace del email.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "email.clicked"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "email.unsubscribed": {
      "post": {
        "summary": "Baja de email",
        "description": "El destinatario se ha dado de baja de los emails.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "email.unsubscribed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "email.bounced": {
      "post": {
        "summary": "Email rebotado",
        "description": "El email ha rebotado.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "email.bounced"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "sms.sent": {
      "post": {
        "summary": "SMS enviado",
        "description": "Se ha enviado un SMS.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "sms.sent"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "sms.failed": {
      "post": {
        "summary": "SMS fallido",
        "description": "Un SMS no se ha podido enviar.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "sms.failed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "sms.unsubscribed": {
      "post": {
        "summary": "Baja de SMS",
        "description": "El destinatario ha pedido no recibir SMS.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "sms.unsubscribed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "whatsapp.unsubscribed": {
      "post": {
        "summary": "Baja de WhatsApp",
        "description": "El destinatario ha pedido no recibir WhatsApp.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "whatsapp.unsubscribed"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    },
    "campaign.finished": {
      "post": {
        "summary": "Campaña terminada",
        "description": "Una campaña ha terminado de enviarse.\n\nEntrega POST firmada con X-PT-Signature (HMAC-SHA256 de \"<t>.<cuerpo>\"). Responde 2xx en menos de 5 s.",
        "tags": [
          "Comunicaciones"
        ],
        "parameters": [
          {
            "name": "X-PT-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "campaign.finished"
              ]
            }
          },
          {
            "name": "X-PT-Delivery-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-PT-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1760000000,v1=5f1c…e9a2"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEnvelope"
              }
            }
          }
        },
        "responses": {
          "2xx": {
            "description": "Recibido"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "pt_live_… / pt_test_…"
      },
      "apiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Código estable del error",
                "example": "insufficient_scope"
              },
              "message": {
                "type": "string",
                "description": "Mensaje para personas (puede cambiar)",
                "example": "La clave API no tiene el permiso «budgets:read»."
              }
            },
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": false
          }
        },
        "description": "Formato uniforme de error",
        "required": [
          "error"
        ],
        "additionalProperties": false
      },
      "ValidationError": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "validation_error"
                ],
                "example": "validation_error"
              },
              "message": {
                "type": "string",
                "example": "El cuerpo de la petición no es válido: revisa la lista «fields»."
              },
              "fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "path": {
                      "type": "string",
                      "description": "Ruta del campo (lines[0].quantity)",
                      "example": "phone"
                    },
                    "message": {
                      "type": "string",
                      "example": "El formato no es válido."
                    }
                  },
                  "required": [
                    "path",
                    "message"
                  ],
                  "additionalProperties": false
                }
              }
            },
            "required": [
              "code",
              "message",
              "fields"
            ],
            "additionalProperties": false
          }
        },
        "description": "422: cuerpo que no cumple el esquema",
        "required": [
          "error"
        ],
        "additionalProperties": false
      },
      "Page": {
        "type": "object",
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cursor opaco para pedir la página siguiente; null si no hay más",
            "example": "aWQ6MTIzNA"
          },
          "has_more": {
            "type": "boolean",
            "description": "true si quedan más resultados",
            "example": true
          }
        },
        "description": "Campos de paginación que acompañan a `data` en todos los listados",
        "required": [
          "next_cursor",
          "has_more"
        ],
        "additionalProperties": false
      },
      "StaffRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id del usuario del taller",
            "example": 3
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nombre visible",
            "example": "Marta"
          }
        },
        "description": "Persona del taller: solo id y nombre",
        "required": [
          "id",
          "name"
        ],
        "additionalProperties": false
      },
      "ClientRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1204
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Laura Gómez"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "additionalProperties": false
      },
      "VehicleRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 871
          },
          "plate": {
            "type": [
              "string",
              "null"
            ],
            "example": "1234 KLM"
          },
          "brand": {
            "type": [
              "string",
              "null"
            ],
            "example": "Seat"
          },
          "model": {
            "type": [
              "string",
              "null"
            ],
            "example": "León"
          }
        },
        "required": [
          "id",
          "plate",
          "brand",
          "model"
        ],
        "additionalProperties": false
      },
      "BoxRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 2
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Elevador 2"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "additionalProperties": false
      },
      "CategoryRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 5
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Mantenimiento"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "additionalProperties": false
      },
      "Client": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1204
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Laura Gómez"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Teléfono principal en formato internacional cuando se conoce",
            "example": "+34600111222"
          },
          "phone_secondary": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "whatsapp_phone": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "example": "laura@ejemplo.com"
          },
          "billing_email": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "tax_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "NIF/CIF",
            "example": "12345678Z"
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "example": "C/ Mayor 12"
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "example": "Barcelona"
          },
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "example": "08001"
          },
          "province": {
            "type": [
              "string",
              "null"
            ],
            "example": "Barcelona"
          },
          "country": {
            "type": [
              "string",
              "null"
            ],
            "example": "ES"
          },
          "preferred_language": {
            "type": [
              "string",
              "null"
            ],
            "description": "Idioma de las comunicaciones (es, ca, en, pt, fr, bg)",
            "example": "es"
          },
          "preferred_contact_method": {
            "type": [
              "string",
              "null"
            ],
            "example": "whatsapp"
          },
          "vip": {
            "type": "boolean",
            "example": false
          },
          "marketing_opt_out": {
            "type": "boolean",
            "description": "No quiere comunicaciones comerciales",
            "example": false
          },
          "channel_opt_out": {
            "type": "object",
            "properties": {
              "email": {
                "type": "boolean",
                "example": false
              },
              "sms": {
                "type": "boolean",
                "example": false
              },
              "whatsapp": {
                "type": "boolean",
                "example": false
              },
              "call": {
                "type": "boolean",
                "example": false
              }
            },
            "description": "Canales que el cliente ha pedido no usar",
            "required": [
              "email",
              "sms",
              "whatsapp",
              "call"
            ],
            "additionalProperties": false
          },
          "erased_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Fecha de la solicitud de borrado RGPD; los datos personales ya están anonimizados",
            "example": null
          },
          "vehicles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Vehicle"
            },
            "description": "Solo con ?expand=vehicles"
          }
        },
        "description": "Cliente",
        "required": [
          "id",
          "name",
          "phone",
          "phone_secondary",
          "whatsapp_phone",
          "email",
          "billing_email",
          "tax_id",
          "address",
          "city",
          "zip",
          "province",
          "country",
          "preferred_language",
          "preferred_contact_method",
          "vip",
          "marketing_opt_out",
          "channel_opt_out",
          "erased_at",
          "vehicles"
        ],
        "additionalProperties": false
      },
      "Vehicle": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 871
          },
          "client_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1204
          },
          "client": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "plate": {
            "type": [
              "string",
              "null"
            ],
            "example": "1234 KLM"
          },
          "vin": {
            "type": [
              "string",
              "null"
            ],
            "description": "Número de bastidor",
            "example": "VSSZZZ5FZJR123456"
          },
          "brand": {
            "type": [
              "string",
              "null"
            ],
            "example": "Seat"
          },
          "model": {
            "type": [
              "string",
              "null"
            ],
            "example": "León"
          },
          "variant": {
            "type": [
              "string",
              "null"
            ],
            "example": "1.5 TSI"
          },
          "year": {
            "type": [
              "integer",
              "null"
            ],
            "example": 2019
          },
          "registration_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "example": "2019-03-15"
          },
          "fuel": {
            "type": [
              "string",
              "null"
            ],
            "example": "Gasolina"
          },
          "transmission": {
            "type": [
              "string",
              "null"
            ],
            "example": "Manual"
          },
          "engine_code": {
            "type": [
              "string",
              "null"
            ],
            "example": "DADA"
          },
          "horsepower": {
            "type": [
              "integer",
              "null"
            ],
            "example": 130
          },
          "displacement": {
            "type": [
              "string",
              "null"
            ],
            "example": "1498"
          },
          "color_code": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "environmental_label": {
            "type": [
              "string",
              "null"
            ],
            "description": "Etiqueta DGT",
            "example": "C"
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Último kilometraje anotado en un presupuesto",
            "example": 84500
          },
          "itv_expiry_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Vencimiento de la ITV",
            "example": "2027-03-15"
          },
          "status": {
            "type": "string",
            "description": "activo, baja_temporal o baja",
            "example": "activo"
          }
        },
        "description": "Vehículo",
        "required": [
          "id",
          "client_id",
          "client",
          "plate",
          "vin",
          "brand",
          "model",
          "variant",
          "year",
          "registration_date",
          "fuel",
          "transmission",
          "engine_code",
          "horsepower",
          "displacement",
          "color_code",
          "environmental_label",
          "km",
          "itv_expiry_date",
          "status"
        ],
        "additionalProperties": false
      },
      "BudgetLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 5501
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "example": "Cambio de aceite y filtro"
          },
          "extended_detail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Detalle ampliado visible al cliente",
            "example": null
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Referencia de recambio",
            "example": null
          },
          "line_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "labor, part, diagnosis, other…",
            "example": "labor"
          },
          "group_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Título del grupo de partidas",
            "example": null
          },
          "quantity": {
            "type": "number",
            "example": 1
          },
          "unit_price": {
            "type": "number",
            "description": "Precio unitario de venta sin impuestos",
            "example": 65
          },
          "discount_pct": {
            "type": "number",
            "description": "Descuento en porcentaje",
            "example": 0
          },
          "tax_rate": {
            "type": "number",
            "description": "Tipo impositivo aplicado (IVA/IGIC)",
            "example": 21
          },
          "tax_exempt_code": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "price_estimated": {
            "type": "boolean",
            "description": "Precio pendiente de confirmar",
            "example": false
          },
          "base": {
            "type": "number",
            "description": "Base de la línea con el descuento aplicado",
            "example": 65
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "sort_order": {
            "type": "integer",
            "example": 0
          }
        },
        "description": "Partida de presupuesto (nunca incluye el coste)",
        "required": [
          "id",
          "description",
          "extended_detail",
          "reference",
          "line_type",
          "group_title",
          "quantity",
          "unit_price",
          "discount_pct",
          "tax_rate",
          "tax_exempt_code",
          "price_estimated",
          "base",
          "currency",
          "sort_order"
        ],
        "additionalProperties": false
      },
      "Totals": {
        "type": "object",
        "properties": {
          "base": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 65
          },
          "tax": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 13.65
          },
          "total": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 78.65
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "tax_rates": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "rate": {
                  "type": "number",
                  "example": 21
                },
                "base": {
                  "type": "number",
                  "description": "Importe en euros con dos decimales",
                  "example": 65
                },
                "quota": {
                  "type": "number",
                  "description": "Importe en euros con dos decimales",
                  "example": 13.65
                }
              },
              "required": [
                "rate",
                "base",
                "quota"
              ],
              "additionalProperties": false
            }
          }
        },
        "description": "Totales con desglose por tipo impositivo",
        "required": [
          "base",
          "tax",
          "total",
          "currency",
          "tax_rates"
        ],
        "additionalProperties": false
      },
      "Appointment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador de la cita: b<presupuesto> si está confirmada, p<propuesta> si está pendiente",
            "example": "b1234"
          },
          "budget_id": {
            "type": "integer",
            "example": 1234
          },
          "status": {
            "type": "string",
            "enum": [
              "confirmed",
              "proposed"
            ],
            "example": "confirmed"
          },
          "start": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T10:30:00.000Z"
          },
          "estimated_duration_minutes": {
            "type": [
              "integer",
              "null"
            ],
            "example": 60
          },
          "client_confirmed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo confirmó el cliente (null si solo la fijó el taller)",
            "example": null
          },
          "budget_status": {
            "type": [
              "string",
              "null"
            ],
            "example": "Aprobado"
          },
          "checked_in_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entrada real del vehículo",
            "example": null
          },
          "client": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "vehicle": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VehicleRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "mechanic": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StaffRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "box": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/BoxRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "category": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CategoryRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "channel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Canal por el que llegó la propuesta (web, portal, voice…)",
            "example": null
          },
          "proposed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo se propuso (solo status=proposed)",
            "example": null
          }
        },
        "description": "Cita confirmada o propuesta pendiente",
        "required": [
          "id",
          "budget_id",
          "status",
          "start",
          "end",
          "estimated_duration_minutes",
          "client_confirmed_at",
          "budget_status",
          "checked_in_at",
          "client",
          "vehicle",
          "mechanic",
          "box",
          "category",
          "channel",
          "proposed_at"
        ],
        "additionalProperties": false
      },
      "BudgetAppointment": {
        "type": "object",
        "properties": {
          "start": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T10:30:00.000Z"
          },
          "status": {
            "type": "string",
            "enum": [
              "confirmed"
            ],
            "example": "confirmed"
          },
          "client_confirmed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "estimated_duration_minutes": {
            "type": [
              "integer",
              "null"
            ],
            "example": 60
          },
          "box": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/BoxRef"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "start",
          "end",
          "status",
          "client_confirmed_at",
          "estimated_duration_minutes",
          "box"
        ],
        "additionalProperties": false
      },
      "Budget": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1234
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pendiente, En cotización, En curso, En espera, Enviado, Aprobado, Finalizado, Facturado, Facturado externamente, Rechazado, Cancelado, Desistido",
            "example": "Aprobado"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "channel": {
            "type": [
              "string",
              "null"
            ],
            "description": "Origen: web, email, teléfono, asistente, API…",
            "example": "web"
          },
          "client_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1204
          },
          "client": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "vehicle_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 871
          },
          "vehicle": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VehicleRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "category": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CategoryRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "subcategory": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CategoryRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "assigned_user": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StaffRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "mechanic": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StaffRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "appointment": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/BudgetAppointment"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_in": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entrada del vehículo en el taller",
            "example": null
          },
          "date_out": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Salida del vehículo",
            "example": null
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "example": 84500
          },
          "client_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Referencia que el cliente quiere ver en la factura",
            "example": null
          },
          "waiting_parts": {
            "type": "boolean",
            "example": false
          },
          "on_hold": {
            "type": "boolean",
            "example": false
          },
          "hold_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "client_signed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Firma del cliente al aceptar",
            "example": null
          },
          "estimated_ready_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Hora prevista de entrega del vehículo (recepción)",
            "example": null
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entrega del vehículo al cliente",
            "example": null
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          }
        },
        "description": "Presupuesto (resumen)",
        "required": [
          "id",
          "status",
          "created_at",
          "updated_at",
          "channel",
          "client_id",
          "client",
          "vehicle_id",
          "vehicle",
          "category",
          "subcategory",
          "assigned_user",
          "mechanic",
          "appointment",
          "date_in",
          "date_out",
          "km",
          "client_reference",
          "waiting_parts",
          "on_hold",
          "hold_until",
          "client_signed_at",
          "estimated_ready_at",
          "delivered_at",
          "reject_reason"
        ],
        "additionalProperties": false
      },
      "BudgetDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1234
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "example": "Aprobado"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "channel": {
            "type": [
              "string",
              "null"
            ],
            "example": "web"
          },
          "client_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1204
          },
          "client": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "vehicle_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 871
          },
          "vehicle": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VehicleRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "category": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CategoryRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "subcategory": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/CategoryRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "assigned_user": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StaffRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "mechanic": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StaffRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "appointment": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/BudgetAppointment"
              },
              {
                "type": "null"
              }
            ]
          },
          "date_in": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "date_out": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "example": 84500
          },
          "client_reference": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "waiting_parts": {
            "type": "boolean",
            "example": false
          },
          "on_hold": {
            "type": "boolean",
            "example": false
          },
          "hold_until": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "client_signed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "estimated_ready_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "public_notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Observaciones visibles para el cliente",
            "example": "Revisar también el ruido en la suspensión."
          },
          "totals": {
            "$ref": "#/components/schemas/Totals"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BudgetLine"
            }
          }
        },
        "description": "Presupuesto con partidas y totales",
        "required": [
          "id",
          "status",
          "created_at",
          "updated_at",
          "channel",
          "client_id",
          "client",
          "vehicle_id",
          "vehicle",
          "category",
          "subcategory",
          "assigned_user",
          "mechanic",
          "appointment",
          "date_in",
          "date_out",
          "km",
          "client_reference",
          "waiting_parts",
          "on_hold",
          "hold_until",
          "client_signed_at",
          "estimated_ready_at",
          "delivered_at",
          "reject_reason",
          "public_notes",
          "totals",
          "lines"
        ],
        "additionalProperties": false
      },
      "InvoiceLine": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 9001
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "example": "Cambio de aceite y filtro"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "group_title": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "quantity": {
            "type": "number",
            "example": 1
          },
          "unit_price": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 65
          },
          "discount_pct": {
            "type": "number",
            "example": 0
          },
          "tax_rate": {
            "type": "number",
            "example": 21
          },
          "tax_exempt_code": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "base": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 65
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "sort_order": {
            "type": "integer",
            "example": 0
          }
        },
        "required": [
          "id",
          "description",
          "reference",
          "group_title",
          "quantity",
          "unit_price",
          "discount_pct",
          "tax_rate",
          "tax_exempt_code",
          "base",
          "currency",
          "sort_order"
        ],
        "additionalProperties": false
      },
      "InvoicePayments": {
        "type": "object",
        "properties": {
          "paid": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 78.65
          },
          "pending": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 0
          },
          "settled": {
            "type": "boolean",
            "example": true
          }
        },
        "description": "Resumen de cobros",
        "required": [
          "paid",
          "pending",
          "settled"
        ],
        "additionalProperties": false
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 412
          },
          "number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Número dentro de la serie",
            "example": 87
          },
          "series": {
            "type": [
              "string",
              "null"
            ],
            "example": "F26"
          },
          "full_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Serie + número",
            "example": "F2687"
          },
          "kind": {
            "type": "string",
            "enum": [
              "invoice",
              "rectification",
              "simplified",
              "other"
            ],
            "example": "invoice"
          },
          "rectifies_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Número de la factura rectificada",
            "example": null
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "issued",
              "cancelled",
              "unknown"
            ],
            "example": "issued"
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "example": "2026-10-06"
          },
          "issued_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "client_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1204
          },
          "client": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "budget_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1234
          },
          "vehicle_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 871
          },
          "vehicle": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VehicleRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "plate": {
            "type": [
              "string",
              "null"
            ],
            "example": "1234 KLM"
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "example": 84500
          },
          "total": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 78.65
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "payments": {
            "$ref": "#/components/schemas/InvoicePayments"
          },
          "payment_method": {
            "type": [
              "string",
              "null"
            ],
            "example": "Tarjeta"
          },
          "rebu": {
            "type": "boolean",
            "description": "Régimen especial de bienes usados",
            "example": false
          },
          "verifactu_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Huella Verifactu de la factura emitida",
            "example": "3f9a…"
          },
          "registered_externally": {
            "type": "boolean",
            "description": "Emitida fuera del programa y registrada con su número original; no tiene huella Verifactu del programa",
            "example": false
          }
        },
        "description": "Factura (resumen)",
        "required": [
          "id",
          "number",
          "series",
          "full_number",
          "kind",
          "rectifies_number",
          "status",
          "date",
          "issued_at",
          "client_id",
          "client",
          "budget_id",
          "vehicle_id",
          "vehicle",
          "plate",
          "km",
          "total",
          "currency",
          "payments",
          "payment_method",
          "rebu",
          "verifactu_hash",
          "registered_externally"
        ],
        "additionalProperties": false
      },
      "InvoiceDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 412
          },
          "number": {
            "type": [
              "integer",
              "null"
            ],
            "example": 87
          },
          "series": {
            "type": [
              "string",
              "null"
            ],
            "example": "F26"
          },
          "full_number": {
            "type": [
              "string",
              "null"
            ],
            "example": "F2687"
          },
          "kind": {
            "type": "string",
            "enum": [
              "invoice",
              "rectification",
              "simplified",
              "other"
            ],
            "example": "invoice"
          },
          "rectifies_number": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "issued",
              "cancelled",
              "unknown"
            ],
            "example": "issued"
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "example": "2026-10-06"
          },
          "issued_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "client_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1204
          },
          "client": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "budget_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1234
          },
          "vehicle_id": {
            "type": [
              "integer",
              "null"
            ],
            "example": 871
          },
          "vehicle": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/VehicleRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "plate": {
            "type": [
              "string",
              "null"
            ],
            "example": "1234 KLM"
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "example": 84500
          },
          "total": {
            "type": "number",
            "description": "Importe en euros con dos decimales",
            "example": 78.65
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "payments": {
            "$ref": "#/components/schemas/InvoicePayments"
          },
          "payment_method": {
            "type": [
              "string",
              "null"
            ],
            "example": "Tarjeta"
          },
          "rebu": {
            "type": "boolean",
            "example": false
          },
          "verifactu_hash": {
            "type": [
              "string",
              "null"
            ],
            "example": "3f9a…"
          },
          "registered_externally": {
            "type": "boolean",
            "example": false
          },
          "billing": {
            "type": "object",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Laura Gómez"
              },
              "tax_id": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "12345678Z"
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "C/ Mayor 12"
              },
              "city": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Barcelona"
              },
              "zip": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "08001"
              },
              "province": {
                "type": [
                  "string",
                  "null"
                ],
                "example": "Barcelona"
              }
            },
            "description": "Datos fiscales tal como quedaron en la factura",
            "required": [
              "name",
              "tax_id",
              "address",
              "city",
              "zip",
              "province"
            ],
            "additionalProperties": false
          },
          "date_in": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "date_out": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "public_notes": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "totals": {
            "$ref": "#/components/schemas/Totals"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceLine"
            }
          },
          "payment_list": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 77
                },
                "date": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time",
                  "example": "2026-10-06T09:30:00.000Z"
                },
                "amount": {
                  "type": "number",
                  "description": "Importe en euros con dos decimales",
                  "example": 78.65
                },
                "currency": {
                  "type": "string",
                  "enum": [
                    "EUR"
                  ],
                  "example": "EUR"
                },
                "method": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "example": "Tarjeta"
                }
              },
              "required": [
                "id",
                "date",
                "amount",
                "currency",
                "method"
              ],
              "additionalProperties": false
            }
          }
        },
        "description": "Factura con líneas y cobros",
        "required": [
          "id",
          "number",
          "series",
          "full_number",
          "kind",
          "rectifies_number",
          "status",
          "date",
          "issued_at",
          "client_id",
          "client",
          "budget_id",
          "vehicle_id",
          "vehicle",
          "plate",
          "km",
          "total",
          "currency",
          "payments",
          "payment_method",
          "rebu",
          "verifactu_hash",
          "registered_externally",
          "billing",
          "date_in",
          "date_out",
          "public_notes",
          "totals",
          "lines",
          "payment_list"
        ],
        "additionalProperties": false
      },
      "ServiceCategory": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 5
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Mantenimiento"
          },
          "reference_price": {
            "type": [
              "number",
              "null"
            ],
            "description": "Precio orientativo de venta, si el taller lo ha fijado",
            "example": 120
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "subcategories": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer",
                  "example": 51
                },
                "name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "example": "Cambio de aceite"
                },
                "reference_price": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "example": 65
                },
                "currency": {
                  "type": "string",
                  "enum": [
                    "EUR"
                  ],
                  "example": "EUR"
                }
              },
              "required": [
                "id",
                "name",
                "reference_price",
                "currency"
              ],
              "additionalProperties": false
            }
          }
        },
        "description": "Categoría de servicio con sus subcategorías",
        "required": [
          "id",
          "name",
          "reference_price",
          "currency",
          "subcategories"
        ],
        "additionalProperties": false
      },
      "LaborRate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "name": {
            "type": "string",
            "example": "Mano de obra general"
          },
          "price_per_hour": {
            "type": [
              "number",
              "null"
            ],
            "description": "Precio de venta por hora sin impuestos",
            "example": 48
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "is_default": {
            "type": "boolean",
            "example": true
          }
        },
        "description": "Tarifa de mano de obra (solo precio de venta)",
        "required": [
          "id",
          "name",
          "price_per_hour",
          "currency",
          "is_default"
        ],
        "additionalProperties": false
      },
      "StockItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 88
          },
          "name": {
            "type": "string",
            "example": "Turbocompresor"
          },
          "brand": {
            "type": [
              "string",
              "null"
            ],
            "example": "Garrett"
          },
          "references": {
            "type": "object",
            "properties": {
              "original": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Referencia del fabricante del vehículo (OEM)",
                "example": "A6540907801"
              },
              "supplier": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Referencia del distribuidor",
                "example": "MA654 090 78 01"
              },
              "own": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Referencia interna del taller",
                "example": null
              }
            },
            "description": "Referencias de la pieza",
            "required": [
              "original",
              "supplier",
              "own"
            ],
            "additionalProperties": false
          },
          "ean": {
            "type": [
              "string",
              "null"
            ],
            "description": "Código de barras EAN-13",
            "example": null
          },
          "unit": {
            "type": "string",
            "enum": [
              "ud",
              "par",
              "juego",
              "caja",
              "rollo",
              "L",
              "kg",
              "g",
              "m"
            ],
            "description": "Unidad de medida. ud, par, juego, caja y rollo solo admiten cantidades enteras; L, kg, g y m admiten hasta 3 decimales",
            "example": "ud"
          },
          "quantity": {
            "type": "number",
            "description": "Cantidad física en el almacén (hasta 3 decimales)",
            "example": 4.5
          },
          "reserved": {
            "type": "number",
            "description": "Bloqueado por presupuestos aceptados o en curso",
            "example": 1
          },
          "pending": {
            "type": "number",
            "description": "Lo que falta en esos presupuestos aceptados porque no había stock (pendiente de pedir)",
            "example": 0
          },
          "planned": {
            "type": "number",
            "description": "Previsto en presupuestos abiertos (Pendiente, En cotización, Enviado): no bloquea",
            "example": 2
          },
          "available": {
            "type": "number",
            "description": "Disponible = quantity − reserved",
            "example": 3.5
          },
          "min_stock": {
            "type": [
              "number",
              "null"
            ],
            "description": "Stock mínimo configurado (null si no hay)",
            "example": 2
          },
          "location": {
            "type": [
              "string",
              "null"
            ],
            "description": "Ubicación en el almacén",
            "example": "Estantería B3"
          },
          "status": {
            "type": "string",
            "description": "nuevo, usado o reacondicionado",
            "example": "nuevo"
          },
          "sale_price": {
            "type": [
              "number",
              "null"
            ],
            "description": "Precio de venta por unidad sin impuestos",
            "example": 2190
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "active": {
            "type": "boolean",
            "description": "false si la pieza está dada de baja del inventario",
            "example": true
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          }
        },
        "description": "Recambio del stock (sin coste de compra ni proveedor)",
        "required": [
          "id",
          "name",
          "brand",
          "references",
          "ean",
          "unit",
          "quantity",
          "reserved",
          "pending",
          "planned",
          "available",
          "min_stock",
          "location",
          "status",
          "sale_price",
          "currency",
          "active",
          "updated_at"
        ],
        "additionalProperties": false
      },
      "DaySchedule": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "example": "08:00"
          },
          "end": {
            "type": "string",
            "example": "19:00"
          },
          "closed": {
            "type": "boolean",
            "example": false
          }
        },
        "required": [
          "start",
          "end",
          "closed"
        ],
        "additionalProperties": false
      },
      "Holiday": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "example": "2026-10-12"
          },
          "name": {
            "type": "string",
            "example": "Fiesta Nacional de España"
          },
          "scope": {
            "type": "string",
            "enum": [
              "nacional",
              "autonomico",
              "local"
            ],
            "example": "nacional"
          }
        },
        "required": [
          "date",
          "name",
          "scope"
        ],
        "additionalProperties": false
      },
      "Workshop": {
        "type": "object",
        "properties": {
          "name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Taller Ejemplo"
          },
          "legal_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "Taller Ejemplo S.L."
          },
          "tax_id": {
            "type": [
              "string",
              "null"
            ],
            "example": "B12345678"
          },
          "address": {
            "type": [
              "string",
              "null"
            ],
            "example": "C/ Industria 4"
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "example": "Barcelona"
          },
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "example": "08020"
          },
          "province": {
            "type": [
              "string",
              "null"
            ],
            "example": "Barcelona"
          },
          "phone": {
            "type": [
              "string",
              "null"
            ],
            "example": "+34931234567"
          },
          "whatsapp": {
            "type": [
              "string",
              "null"
            ],
            "example": "+34600111222"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "example": "taller@ejemplo.com"
          },
          "web": {
            "type": [
              "string",
              "null"
            ],
            "example": "https://www.ejemplo.com"
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "timezone": {
            "type": "string",
            "example": "Europe/Madrid"
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "booking_mode": {
            "type": "string",
            "enum": [
              "propose",
              "book"
            ],
            "description": "propose: el cliente propone y el taller confirma; book: el cliente reserva directamente",
            "example": "propose"
          },
          "schedule": {
            "type": "object",
            "properties": {
              "mon": {
                "$ref": "#/components/schemas/DaySchedule"
              },
              "tue": {
                "$ref": "#/components/schemas/DaySchedule"
              },
              "wed": {
                "$ref": "#/components/schemas/DaySchedule"
              },
              "thu": {
                "$ref": "#/components/schemas/DaySchedule"
              },
              "fri": {
                "$ref": "#/components/schemas/DaySchedule"
              },
              "sat": {
                "$ref": "#/components/schemas/DaySchedule"
              },
              "sun": {
                "$ref": "#/components/schemas/DaySchedule"
              }
            },
            "description": "Horario semanal de la agenda",
            "required": [
              "mon",
              "tue",
              "wed",
              "thu",
              "fri",
              "sat",
              "sun"
            ],
            "additionalProperties": false
          },
          "lunch_break": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "start": {
                    "type": "string",
                    "example": "13:30"
                  },
                  "end": {
                    "type": "string",
                    "example": "15:00"
                  }
                },
                "required": [
                  "start",
                  "end"
                ],
                "additionalProperties": false
              },
              {
                "type": "null"
              }
            ]
          },
          "min_slot_minutes": {
            "type": "integer",
            "example": 60
          },
          "upcoming_holidays": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Holiday"
            },
            "description": "Festivos de los próximos 90 días (nacionales, autonómicos y los del taller)"
          }
        },
        "description": "Datos públicos del taller",
        "required": [
          "name",
          "legal_name",
          "tax_id",
          "address",
          "city",
          "zip",
          "province",
          "phone",
          "whatsapp",
          "email",
          "web",
          "logo_url",
          "timezone",
          "currency",
          "booking_mode",
          "schedule",
          "lunch_break",
          "min_slot_minutes",
          "upcoming_holidays"
        ],
        "additionalProperties": false
      },
      "Communication": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "c_8812"
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "sms",
              "whatsapp",
              "call",
              "push"
            ],
            "example": "email"
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ],
            "example": "out"
          },
          "recipient": {
            "type": [
              "string",
              "null"
            ],
            "example": "laura@ejemplo.com"
          },
          "subject": {
            "type": [
              "string",
              "null"
            ],
            "example": "Su presupuesto"
          },
          "body": {
            "type": [
              "string",
              "null"
            ],
            "description": "Contenido completo",
            "example": "Hola Laura, …"
          },
          "idlead": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1234
          },
          "idclient": {
            "type": [
              "integer",
              "null"
            ],
            "example": 1204
          },
          "clientName": {
            "type": [
              "string",
              "null"
            ],
            "example": "Laura Gómez"
          },
          "status": {
            "type": "string",
            "example": "sent"
          },
          "created_at": {
            "type": "string",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "duration": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Segundos (solo llamadas)",
            "example": null
          },
          "recordingUrl": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "agent": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "fromNumber": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "toNumber": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          }
        },
        "description": "Comunicación registrada",
        "required": [
          "id",
          "channel",
          "direction",
          "recipient",
          "subject",
          "body",
          "idlead",
          "idclient",
          "clientName",
          "status",
          "created_at",
          "duration",
          "recordingUrl",
          "agent",
          "fromNumber",
          "toNumber"
        ],
        "additionalProperties": false
      },
      "Ping": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "example": true
          },
          "key": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer",
                "example": 7
              },
              "name": {
                "type": "string",
                "example": "CRM"
              },
              "environment": {
                "type": "string",
                "enum": [
                  "live",
                  "test"
                ],
                "example": "live"
              },
              "scopes": {
                "type": "array",
                "items": {
                  "type": "string",
                  "example": "clients:read"
                }
              },
              "expires_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "example": null
              },
              "client": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/ClientRef"
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "Clave de cliente: el único cliente cuyos datos puede ver y tocar. null = clave del taller (todos los clientes)."
              }
            },
            "required": [
              "id",
              "name",
              "environment",
              "scopes",
              "expires_at",
              "client"
            ],
            "additionalProperties": false
          },
          "rate_limit": {
            "type": "object",
            "properties": {
              "per_minute": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "example": 60
                  },
                  "used": {
                    "type": "integer",
                    "example": 1
                  },
                  "remaining": {
                    "type": "integer",
                    "example": 59
                  }
                },
                "required": [
                  "limit",
                  "used",
                  "remaining"
                ],
                "additionalProperties": false
              },
              "per_day": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "example": 20000
                  },
                  "used": {
                    "type": "integer",
                    "example": 1
                  },
                  "remaining": {
                    "type": "integer",
                    "example": 19999
                  }
                },
                "required": [
                  "limit",
                  "used",
                  "remaining"
                ],
                "additionalProperties": false
              }
            },
            "required": [
              "per_minute",
              "per_day"
            ],
            "additionalProperties": false
          },
          "instance": {
            "type": "string",
            "description": "Identificador de la instancia (host)",
            "example": "taller.ejemplo.com"
          },
          "server_time": {
            "type": "string",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "version": {
            "type": "string",
            "description": "Versión de la API",
            "example": "v1"
          }
        },
        "description": "Estado de la clave",
        "required": [
          "ok",
          "key",
          "rate_limit",
          "instance",
          "server_time",
          "version"
        ],
        "additionalProperties": false
      },
      "Slot": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "description": "Inicio (ISO 8601 UTC)",
            "example": "2026-10-14T07:00:00.000Z",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "example": "2026-10-14T08:00:00.000Z",
            "format": "date-time"
          },
          "box": {
            "$ref": "#/components/schemas/BoxRef"
          }
        },
        "description": "Hueco libre",
        "required": [
          "start",
          "end",
          "box"
        ],
        "additionalProperties": false
      },
      "AppointmentAvailability": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Slot"
            }
          },
          "duration_minutes": {
            "type": "integer",
            "example": 60
          },
          "from": {
            "type": "string",
            "example": "2026-10-14T00:00:00.000Z",
            "format": "date-time"
          },
          "to": {
            "type": "string",
            "example": "2026-10-21T00:00:00.000Z",
            "format": "date-time"
          },
          "booking_mode": {
            "type": "string",
            "enum": [
              "propose",
              "book"
            ],
            "example": "propose"
          }
        },
        "description": "Huecos libres con la misma lógica que la agenda",
        "required": [
          "data",
          "duration_minutes",
          "from",
          "to",
          "booking_mode"
        ],
        "additionalProperties": false
      },
      "AppointmentCancelled": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "example": true
          },
          "id": {
            "type": "string",
            "example": "b1234"
          },
          "status": {
            "type": "string",
            "enum": [
              "cancelled",
              "withdrawn"
            ],
            "description": "cancelled: cita confirmada anulada; withdrawn: propuesta retirada",
            "example": "cancelled"
          }
        },
        "required": [
          "ok",
          "id",
          "status"
        ],
        "additionalProperties": false
      },
      "BudgetDocument": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 991
          },
          "budget_id": {
            "type": "integer",
            "example": 1234
          },
          "filename": {
            "type": "string",
            "example": "parte-de-trabajo.pdf"
          },
          "content_type": {
            "type": "string",
            "enum": [
              "image/jpeg",
              "image/png",
              "image/webp",
              "application/pdf"
            ],
            "example": "application/pdf"
          },
          "size": {
            "type": "integer",
            "description": "Bytes",
            "example": 182044
          },
          "url": {
            "type": "string",
            "example": "https://…/leads/1234/api-parte-de-trabajo.pdf"
          },
          "client_visible": {
            "type": "boolean",
            "description": "Visible para el cliente en su enlace de seguimiento",
            "example": false
          },
          "mechanic_visible": {
            "type": "boolean",
            "description": "Visible en la app del mecánico",
            "example": false
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          }
        },
        "description": "Documento adjunto a un presupuesto",
        "required": [
          "id",
          "budget_id",
          "filename",
          "content_type",
          "size",
          "url",
          "client_visible",
          "mechanic_visible",
          "created_at"
        ],
        "additionalProperties": false
      },
      "VehicleHistoryVisit": {
        "type": "object",
        "properties": {
          "budget_id": {
            "type": "integer",
            "example": 1234
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "example": "Facturado"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "checked_in_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entrada en el taller",
            "example": "2026-09-02T07:30:00.000Z"
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Salida del taller",
            "example": "2026-09-03T16:00:00.000Z"
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Kilometraje anotado en la visita",
            "example": 84210
          },
          "amount": {
            "type": [
              "number",
              "null"
            ],
            "description": "Importe del presupuesto (euros, IVA incluido)",
            "example": 245.3
          },
          "invoice": {
            "type": [
              "string",
              "null"
            ],
            "description": "Número de factura si la hay",
            "example": "F26/118"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "example": "Mantenimiento"
          },
          "works": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "Cambio de aceite y filtro"
            },
            "description": "Conceptos de las partidas (sin precios de coste)"
          }
        },
        "description": "Visita del vehículo al taller",
        "required": [
          "budget_id",
          "status",
          "created_at",
          "checked_in_at",
          "delivered_at",
          "km",
          "amount",
          "invoice",
          "category",
          "works"
        ],
        "additionalProperties": false
      },
      "VehicleHistory": {
        "type": "object",
        "properties": {
          "vehicle": {
            "$ref": "#/components/schemas/Vehicle"
          },
          "visits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VehicleHistoryVisit"
            },
            "description": "Presupuestos del vehículo, del más reciente al más antiguo (sin los de uso interno)"
          },
          "external_interventions": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "example": "Cambio de neumáticos"
                },
                "date": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date",
                  "example": "2025-11-20"
                },
                "km": {
                  "type": [
                    "integer",
                    "null"
                  ],
                  "example": 78000
                },
                "workshop_name": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Otro taller (si lo anotó el cliente o el taller)",
                  "example": null
                },
                "warranty_until": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date",
                  "example": null
                }
              },
              "required": [
                "title",
                "date",
                "km",
                "workshop_name",
                "warranty_until"
              ],
              "additionalProperties": false
            },
            "description": "Intervenciones anotadas fuera de este taller"
          }
        },
        "description": "Historial de reparaciones y visitas de un vehículo",
        "required": [
          "vehicle",
          "visits",
          "external_interventions"
        ],
        "additionalProperties": false
      },
      "ActivityEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 88123
          },
          "at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "event": {
            "type": [
              "string",
              "null"
            ],
            "description": "Tipo de evento (status_changed, web_accepted, invoiced, client_chat_message…)",
            "example": "web_accepted"
          },
          "detail": {
            "type": [
              "string",
              "null"
            ],
            "example": "Presupuesto aceptado por el cliente desde la web de seguimiento"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "staff, client, system…",
            "example": "client"
          },
          "public": {
            "type": "boolean",
            "description": "true si el cliente lo ve en su enlace de seguimiento",
            "example": true
          },
          "user": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/StaffRef"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "description": "Entrada del historial del presupuesto",
        "required": [
          "id",
          "at",
          "event",
          "detail",
          "source",
          "public",
          "user"
        ],
        "additionalProperties": false
      },
      "BudgetDecision": {
        "type": "object",
        "properties": {
          "budget": {
            "$ref": "#/components/schemas/BudgetDetail"
          },
          "already": {
            "type": "boolean",
            "description": "true si ya estaba en ese estado (no se ha cambiado nada)",
            "example": false
          },
          "signature": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "waived_by_integration",
              "not_required",
              null
            ],
            "description": "Solo al aceptar: waived_by_integration si el taller exige firma y la API ha aceptado sin ella",
            "example": "not_required"
          }
        },
        "description": "Resultado de aceptar o rechazar en nombre del cliente",
        "required": [
          "budget",
          "already",
          "signature"
        ],
        "additionalProperties": false
      },
      "InvoiceCreated": {
        "type": "object",
        "properties": {
          "invoice": {
            "$ref": "#/components/schemas/InvoiceDetail"
          },
          "created": {
            "type": "boolean",
            "description": "false si el presupuesto ya tenía factura y se devuelve esa",
            "example": true
          }
        },
        "description": "Factura emitida desde un presupuesto",
        "required": [
          "invoice",
          "created"
        ],
        "additionalProperties": false
      },
      "Conversation": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Clave estable: whatsapp:<9 dígitos>, sms:<9 dígitos>, chat:<id> o email:<id>",
            "example": "whatsapp:600111222"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "sms",
              "chat",
              "email"
            ],
            "example": "whatsapp"
          },
          "contact": {
            "type": [
              "string",
              "null"
            ],
            "description": "Número, email o identificador del visitante",
            "example": "+34600111222"
          },
          "client": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ClientRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "budget_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Presupuesto activo de la conversación",
            "example": 1234
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "pending",
              "closed"
            ],
            "description": "open: falta contestar; pending: esperando al cliente; closed: cerrada",
            "example": "open"
          },
          "unread": {
            "type": "integer",
            "example": 1
          },
          "last_message_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "last_inbound_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:29:00.000Z"
          },
          "last_message": {
            "type": [
              "string",
              "null"
            ],
            "example": "¿A qué hora puedo pasar a por el coche?"
          },
          "last_author": {
            "type": "string",
            "enum": [
              "cliente",
              "ia",
              "equipo",
              "automatico"
            ],
            "example": "cliente"
          },
          "whatsapp_window_open": {
            "type": "boolean",
            "description": "Solo WhatsApp: true si se puede escribir texto libre (ventana de 24 h)",
            "example": true
          },
          "external_bot": {
            "type": "boolean",
            "description": "true si el canal está en modo asistente externo",
            "example": false
          }
        },
        "description": "Conversación de la Bandeja",
        "required": [
          "key",
          "channel",
          "contact",
          "client",
          "budget_id",
          "status",
          "unread",
          "last_message_at",
          "last_inbound_at",
          "last_message",
          "last_author",
          "whatsapp_window_open",
          "external_bot"
        ],
        "additionalProperties": false
      },
      "ConversationMessage": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 77120
          },
          "at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ],
            "example": "in"
          },
          "author": {
            "type": "string",
            "enum": [
              "cliente",
              "ia",
              "equipo",
              "automatico",
              "integracion"
            ],
            "example": "cliente"
          },
          "author_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Persona del equipo o integración que escribió",
            "example": null
          },
          "subject": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo email",
            "example": null
          },
          "text": {
            "type": [
              "string",
              "null"
            ],
            "example": "¿A qué hora puedo pasar a por el coche?"
          },
          "attachments": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "https://…/whatsapp-in/SM123-0.jpg"
            },
            "description": "URLs de los adjuntos"
          },
          "status": {
            "type": [
              "string",
              "null"
            ],
            "example": "received"
          }
        },
        "description": "Mensaje de una conversación",
        "required": [
          "id",
          "at",
          "direction",
          "author",
          "author_name",
          "subject",
          "text",
          "attachments",
          "status"
        ],
        "additionalProperties": false
      },
      "MessageSent": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "example": true
          },
          "key": {
            "type": "string",
            "example": "whatsapp:600111222"
          },
          "channel": {
            "type": "string",
            "enum": [
              "whatsapp",
              "sms",
              "chat",
              "email"
            ],
            "example": "whatsapp"
          },
          "mode": {
            "type": "string",
            "enum": [
              "free",
              "template",
              "text",
              "email"
            ],
            "example": "free"
          },
          "simulated": {
            "type": "boolean",
            "description": "true en el entorno de pruebas: no ha salido de verdad",
            "example": false
          }
        },
        "description": "Mensaje enviado",
        "required": [
          "ok",
          "key",
          "channel",
          "mode",
          "simulated"
        ],
        "additionalProperties": false
      },
      "WhatsAppTemplate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 12
          },
          "name": {
            "type": "string",
            "example": "presupuesto_listo"
          },
          "language": {
            "type": "string",
            "example": "es"
          },
          "category": {
            "type": "string",
            "example": "UTILITY"
          },
          "body": {
            "type": "string",
            "description": "Texto con sus etiquetas (#nombre#, #total#…)",
            "example": "Hola #nombre#, su presupuesto está listo."
          },
          "variables": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "#nombre#"
            },
            "description": "Etiquetas que admite template_variables"
          }
        },
        "description": "Plantilla de WhatsApp aprobada por Meta",
        "required": [
          "id",
          "name",
          "language",
          "category",
          "body",
          "variables"
        ],
        "additionalProperties": false
      },
      "AssistantSettings": {
        "type": "object",
        "properties": {
          "external_channels": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "whatsapp",
                "sms",
                "chat",
                "email"
              ],
              "example": "whatsapp"
            },
            "description": "Canales en los que contesta el asistente externo (el del programa deja de hacerlo)"
          },
          "api_key_id": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Clave de la integración que contesta; si se revoca o caduca, vuelve a contestar el asistente del programa",
            "example": 7
          },
          "fallback_minutes": {
            "type": "integer",
            "description": "Minutos sin respuesta de la integración tras los que contesta el asistente del programa",
            "example": 10
          },
          "channels_owned_now": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "whatsapp",
                "sms",
                "chat",
                "email"
              ]
            },
            "description": "Canales que ahora mismo atiende la integración (clave viva y webhook message.received activo)"
          }
        },
        "description": "Modo asistente externo por canal",
        "required": [
          "external_channels",
          "api_key_id",
          "fallback_minutes",
          "channels_owned_now"
        ],
        "additionalProperties": false
      },
      "SandboxKey": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Clave de pruebas: solo se muestra una vez",
            "example": "pt_test_0123456789abcdef0123456789abcdef"
          },
          "expires_at": {
            "type": "string",
            "example": "2026-11-12T10:00:00.000Z",
            "format": "date-time"
          },
          "base_url": {
            "type": "string",
            "example": "https://demo.programa-taller.es/api/v1"
          },
          "docs_url": {
            "type": "string",
            "example": "https://demo.programa-taller.es/api/v1/openapi.json"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "budgets:accept"
            }
          }
        },
        "description": "Clave del entorno de pruebas",
        "required": [
          "key",
          "expires_at",
          "base_url",
          "docs_url",
          "scopes"
        ],
        "additionalProperties": false
      },
      "WebhookEnvelope": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string",
            "description": "Nombre del evento",
            "example": "lead.accepted"
          },
          "timestamp": {
            "type": "string",
            "example": "2026-10-06T10:15:00.000Z"
          },
          "data": {
            "type": "object",
            "description": "Carga específica del evento",
            "additionalProperties": true,
            "example": {
              "leadId": 1234,
              "from": "Enviado",
              "to": "Aprobado"
            }
          }
        },
        "description": "Cuerpo de toda entrega de webhook",
        "required": [
          "event",
          "timestamp",
          "data"
        ],
        "additionalProperties": false
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 3
          },
          "url": {
            "type": "string",
            "example": "https://tu-sistema.com/webhooks/taller"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "example": "lead.accepted"
            }
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "example": "CRM"
          },
          "active": {
            "type": "boolean",
            "example": true
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          }
        },
        "description": "Suscripción a eventos (nunca incluye el secreto)",
        "required": [
          "id",
          "url",
          "events",
          "description",
          "active",
          "created_at"
        ],
        "additionalProperties": false
      }
    },
    "x-scopes": [
      {
        "scope": "clients:read",
        "area": "clients",
        "label": "Leer clientes",
        "description": "Consultar fichas de clientes y sus datos de contacto.",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "clients:write",
        "area": "clients",
        "label": "Crear y editar clientes",
        "description": "Dar de alta clientes nuevos y modificar los existentes.",
        "sideEffect": true,
        "adminOnly": false
      },
      {
        "scope": "vehicles:read",
        "area": "vehicles",
        "label": "Leer vehículos",
        "description": "Consultar vehículos, matrículas y su historial.",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "vehicles:write",
        "area": "vehicles",
        "label": "Crear y editar vehículos",
        "description": "Dar de alta vehículos y modificar sus datos.",
        "sideEffect": true,
        "adminOnly": false
      },
      {
        "scope": "budgets:read",
        "area": "budgets",
        "label": "Leer presupuestos",
        "description": "Consultar presupuestos, sus partidas y su estado.",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "budgets:write",
        "area": "budgets",
        "label": "Crear y editar presupuestos",
        "description": "Crear presupuestos, añadir partidas y cambiar su estado.",
        "sideEffect": true,
        "adminOnly": false
      },
      {
        "scope": "budgets:accept",
        "area": "budgets",
        "label": "Aceptar o rechazar en nombre del cliente",
        "description": "Aceptar un presupuesto en nombre del cliente con su evidencia (quién y por qué canal) o rechazarlo con motivo. Queda en el historial con el nombre de la clave.",
        "sideEffect": true,
        "adminOnly": true,
        "warning": "Permite aceptar presupuestos en nombre del cliente: la integración responde de que el cliente lo ha aceptado de verdad."
      },
      {
        "scope": "invoices:read",
        "area": "invoices",
        "label": "Leer facturas",
        "description": "Consultar facturas emitidas, importes y cobros.",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "invoices:write",
        "area": "invoices",
        "label": "Emitir facturas y anotar cobros",
        "description": "Emitir la factura de un presupuesto (numeración, serie, Verifactu y contabilidad del programa) y registrar cobros.",
        "sideEffect": true,
        "adminOnly": true,
        "warning": "Permite emitir facturas legales con numeración y Verifactu: una factura emitida no se borra, solo se rectifica desde el programa."
      },
      {
        "scope": "appointments:read",
        "area": "appointments",
        "label": "Leer citas",
        "description": "Consultar la agenda de citas y los huecos disponibles.",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "appointments:write",
        "area": "appointments",
        "label": "Reservar y cancelar citas",
        "description": "Crear, mover y cancelar citas en la agenda.",
        "sideEffect": true,
        "adminOnly": false
      },
      {
        "scope": "communications:read",
        "area": "communications",
        "label": "Leer comunicaciones",
        "description": "Consultar el registro de emails, SMS, WhatsApp y llamadas (incluye el contenido completo).",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "communications:send",
        "area": "communications",
        "label": "Enviar email y SMS",
        "description": "Enviar emails y SMS transaccionales desde la instancia; consume saldo.",
        "sideEffect": true,
        "adminOnly": false
      },
      {
        "scope": "messages:send",
        "area": "communications",
        "label": "Responder conversaciones",
        "description": "Responder como el taller en las conversaciones de WhatsApp, SMS, chat web y email (bot o IA externa); consume saldo.",
        "sideEffect": true,
        "adminOnly": true,
        "warning": "Permite escribir a los clientes en nombre del taller por WhatsApp, SMS, chat y email."
      },
      {
        "scope": "catalog:read",
        "area": "catalog",
        "label": "Leer catálogo",
        "description": "Consultar servicios, tarifas y conceptos del tarifario.",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "stock:read",
        "area": "stock",
        "label": "Leer stock",
        "description": "Consultar existencias y referencias de recambios.",
        "sideEffect": false,
        "adminOnly": false
      },
      {
        "scope": "stock:write",
        "area": "stock",
        "label": "Ajustar stock",
        "description": "Dar entradas y salidas de recambios.",
        "sideEffect": true,
        "adminOnly": false
      },
      {
        "scope": "webhooks:manage",
        "area": "webhooks",
        "label": "Gestionar webhooks",
        "description": "Crear, listar y borrar las suscripciones a eventos de esta clave.",
        "sideEffect": true,
        "adminOnly": false
      },
      {
        "scope": "settings:write",
        "area": "settings",
        "label": "Ajustes del asistente externo",
        "description": "Consultar y cambiar en qué canales contesta un asistente externo en lugar del asistente del programa.",
        "sideEffect": true,
        "adminOnly": true,
        "warning": "Permite apagar el asistente del programa en los canales que elija la integración."
      },
      {
        "scope": "reports:read",
        "area": "reports",
        "label": "Leer informes",
        "description": "Consultar cifras agregadas de facturación, actividad y rendimiento.",
        "sideEffect": false,
        "adminOnly": false
      }
    ],
    "x-error-codes": [
      {
        "code": "missing_api_key",
        "status": 401,
        "message": "Falta la clave API: envíala en la cabecera Authorization: Bearer pt_… o X-Api-Key."
      },
      {
        "code": "invalid_api_key",
        "status": 401,
        "message": "La clave API no es válida para esta instancia."
      },
      {
        "code": "revoked_api_key",
        "status": 401,
        "message": "La clave API está revocada."
      },
      {
        "code": "expired_api_key",
        "status": 401,
        "message": "La clave API ha caducado."
      },
      {
        "code": "ip_not_allowed",
        "status": 403,
        "message": "La dirección IP de origen no está en la lista permitida de esta clave."
      },
      {
        "code": "insufficient_scope",
        "status": 403,
        "message": "La clave API no tiene el permiso necesario para esta operación."
      },
      {
        "code": "test_key_forbidden",
        "status": 403,
        "message": "Una clave de prueba (pt_test_) no puede realizar operaciones con efectos: usa una clave live."
      },
      {
        "code": "rate_limited",
        "status": 429,
        "message": "Has superado el límite de peticiones de esta clave. Espera y reintenta."
      },
      {
        "code": "instance_rate_limited",
        "status": 503,
        "message": "La instancia está recibiendo demasiadas peticiones por API en este momento. Reintenta en unos segundos."
      },
      {
        "code": "plan_quota_exceeded",
        "status": 429,
        "message": "Se ha agotado el cupo diario de peticiones del plan de API del taller. Se renueva a las 00:00 UTC; para más volumen, mejora el plan."
      },
      {
        "code": "plan_scope_not_allowed",
        "status": 403,
        "message": "El plan de API del taller no incluye este permiso. Para usarlo hay que mejorar el plan."
      },
      {
        "code": "bad_request",
        "status": 400,
        "message": "La petición no es válida."
      },
      {
        "code": "not_found",
        "status": 404,
        "message": "No se ha encontrado el recurso."
      },
      {
        "code": "upstream_error",
        "status": 502,
        "message": "Un servicio externo ha rechazado la operación."
      },
      {
        "code": "internal_error",
        "status": 500,
        "message": "Error interno."
      },
      {
        "code": "validation_error",
        "status": 422,
        "message": "El cuerpo de la petición no es válido: revisa la lista «fields»."
      },
      {
        "code": "idempotency_key_required",
        "status": 400,
        "message": "Falta la cabecera Idempotency-Key (obligatoria en todo POST; de 8 a 255 caracteres visibles)."
      },
      {
        "code": "idempotency_conflict",
        "status": 409,
        "message": "Esa Idempotency-Key ya se usó en las últimas 24 h con otra petición distinta."
      },
      {
        "code": "idempotency_in_progress",
        "status": 409,
        "message": "Hay otra petición con la misma Idempotency-Key en curso. Reintenta en unos segundos."
      },
      {
        "code": "client_exists",
        "status": 409,
        "message": "Ya existe un cliente con ese teléfono, email o NIF/CIF."
      },
      {
        "code": "client_erased",
        "status": 409,
        "message": "El cliente pidió el borrado de sus datos (RGPD): su ficha no admite cambios ni altas asociadas."
      },
      {
        "code": "vehicle_exists",
        "status": 409,
        "message": "Ese cliente ya tiene un vehículo con esa matrícula."
      },
      {
        "code": "vehicle_belongs_to_other_client",
        "status": 409,
        "message": "Esa matrícula ya está dada de alta a nombre de otro cliente."
      },
      {
        "code": "budget_locked",
        "status": 409,
        "message": "El presupuesto está cerrado y ya no admite este cambio."
      },
      {
        "code": "invalid_status_transition",
        "status": 409,
        "message": "El presupuesto no puede pasar a ese estado desde el estado actual."
      },
      {
        "code": "status_transition_forbidden",
        "status": 403,
        "message": "Ese cambio de estado no está disponible por API."
      },
      {
        "code": "client_acceptance_required",
        "status": 403,
        "message": "Para aceptar un presupuesto usa POST /budgets/{id}/accept con el permiso budgets:accept y la evidencia de quién acepta."
      },
      {
        "code": "booking_mode_propose_only",
        "status": 403,
        "message": "El taller trabaja en modo «proponer cita»: solo se pueden crear propuestas que el taller confirma."
      },
      {
        "code": "slot_unavailable",
        "status": 409,
        "message": "Ese hueco no está disponible."
      },
      {
        "code": "payload_too_large",
        "status": 413,
        "message": "El fichero supera el tamaño máximo (4 MB)."
      },
      {
        "code": "unsupported_media_type",
        "status": 415,
        "message": "Tipo de fichero no admitido: solo JPEG, PNG, WebP o PDF."
      },
      {
        "code": "fiscal_data_incomplete",
        "status": 422,
        "message": "Faltan datos fiscales del cliente para poder facturar: revisa la lista «missing»."
      },
      {
        "code": "budget_not_invoiceable",
        "status": 409,
        "message": "El presupuesto no se puede facturar en su estado actual."
      },
      {
        "code": "invoice_rejected",
        "status": 422,
        "message": "El programa no ha podido emitir la factura: revisa el mensaje."
      },
      {
        "code": "whatsapp_window_closed",
        "status": 422,
        "message": "Han pasado más de 24 h desde el último mensaje del cliente: por WhatsApp solo se le puede escribir con una plantilla aprobada (template_id)."
      },
      {
        "code": "channel_unavailable",
        "status": 409,
        "message": "Ese canal no está configurado en el taller."
      },
      {
        "code": "conversation_not_found",
        "status": 404,
        "message": "No existe esa conversación."
      },
      {
        "code": "sandbox_only",
        "status": 404,
        "message": "Solo disponible en el entorno de pruebas (https://demo.programa-taller.es)."
      },
      {
        "code": "client_key_forbidden",
        "status": 403,
        "message": "Esta clave está limitada a un cliente del taller y no puede hacer esta operación: solo trabaja con los datos de su cliente."
      }
    ]
  },
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyAuth": []
    }
  ]
}
