{
  "openapi": "3.1.0",
  "info": {
    "title": "API pública de Promec",
    "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.",
    "x-languages": [
      "es",
      "en",
      "ca",
      "pt",
      "fr",
      "bg"
    ],
    "x-guides": [
      {
        "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 y tracking_url: compártela con el cliente para que acepte y firme."
          },
          {
            "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."
          }
        ]
      }
    ]
  },
  "servers": [
    {
      "url": "https://tu-taller.example",
      "description": "Instancia del taller"
    }
  ],
  "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": "Comunicaciones",
      "x-area": "communications"
    },
    {
      "name": "Webhooks",
      "x-area": "webhooks"
    }
  ],
  "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 y el estado de sus límites. Vale cualquier clave válida.\n\nCualquier clave válida, sin permiso concreto.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "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.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "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.",
            "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "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/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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "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}/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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "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.",
            "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "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.",
            "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "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.",
            "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 con la tracking_url: la aceptación la firma el cliente. Facturar, rechazar o desistir responden 403 status_transition_forbidden; 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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "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_acceptance_required: La aceptación del presupuesto tiene que hacerla el cliente desde su enlace de seguimiento firmado. | 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}/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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "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/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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "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"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/catalog/services": {
      "get": {
        "operationId": "listServices",
        "tags": [
          "Catálogo"
        ],
        "summary": "Categorías y subcategorías de servicio",
        "description": "Permiso necesario: `catalog:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "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/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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "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": "Sale con la cuenta de correo configurada en el taller y queda en el registro de Comunicación. Si rebota, el email del cliente se marca como no válido.\n\nPermiso necesario: `communications:send`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:send",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "subject"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "format": "email",
                    "example": "cliente@ejemplo.com"
                  },
                  "subject": {
                    "type": "string",
                    "example": "Su vehículo está listo"
                  },
                  "text": {
                    "type": "string",
                    "example": "Puede pasar a recogerlo."
                  },
                  "html": {
                    "type": "string",
                    "example": "<p>Puede pasar a recogerlo.</p>"
                  },
                  "fromName": {
                    "type": "string",
                    "example": "Taller"
                  },
                  "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.",
            "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:send",
        "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.",
            "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": {
      "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "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.\n\nPermiso necesario: `webhooks:manage`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "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`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "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"
          }
        }
      }
    },
    "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"
          }
        }
      }
    },
    "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.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.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"
          }
        }
      }
    },
    "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"
          }
        }
      }
    },
    "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
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entrega del vehículo al cliente",
            "example": null
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "tracking_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL pública de seguimiento firmada",
            "example": "https://taller.ejemplo.com/public/seguimiento?id=…"
          }
        },
        "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",
          "delivered_at",
          "reject_reason",
          "tracking_url"
        ],
        "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
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": null
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "tracking_url": {
            "type": [
              "string",
              "null"
            ],
            "example": "https://taller.ejemplo.com/public/seguimiento?id=…"
          },
          "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",
          "delivered_at",
          "reject_reason",
          "tracking_url",
          "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…"
          }
        },
        "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"
        ],
        "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…"
          },
          "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",
          "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
      },
      "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
              }
            },
            "required": [
              "id",
              "name",
              "environment",
              "scopes",
              "expires_at"
            ],
            "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
      },
      "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
      },
      {
        "scope": "clients:write",
        "area": "clients",
        "label": "Crear y editar clientes",
        "description": "Dar de alta clientes nuevos y modificar los existentes.",
        "sideEffect": true
      },
      {
        "scope": "vehicles:read",
        "area": "vehicles",
        "label": "Leer vehículos",
        "description": "Consultar vehículos, matrículas y su historial.",
        "sideEffect": false
      },
      {
        "scope": "vehicles:write",
        "area": "vehicles",
        "label": "Crear y editar vehículos",
        "description": "Dar de alta vehículos y modificar sus datos.",
        "sideEffect": true
      },
      {
        "scope": "budgets:read",
        "area": "budgets",
        "label": "Leer presupuestos",
        "description": "Consultar presupuestos, sus partidas y su estado.",
        "sideEffect": false
      },
      {
        "scope": "budgets:write",
        "area": "budgets",
        "label": "Crear y editar presupuestos",
        "description": "Crear presupuestos, añadir partidas y cambiar su estado.",
        "sideEffect": true
      },
      {
        "scope": "invoices:read",
        "area": "invoices",
        "label": "Leer facturas",
        "description": "Consultar facturas emitidas, importes y cobros. Emitir facturas no está disponible por API.",
        "sideEffect": false
      },
      {
        "scope": "appointments:read",
        "area": "appointments",
        "label": "Leer citas",
        "description": "Consultar la agenda de citas y los huecos disponibles.",
        "sideEffect": false
      },
      {
        "scope": "appointments:write",
        "area": "appointments",
        "label": "Reservar y cancelar citas",
        "description": "Crear, mover y cancelar citas en la agenda.",
        "sideEffect": true
      },
      {
        "scope": "communications:read",
        "area": "communications",
        "label": "Leer comunicaciones",
        "description": "Consultar el registro de emails, SMS, WhatsApp y llamadas (incluye el contenido completo).",
        "sideEffect": false
      },
      {
        "scope": "communications:send",
        "area": "communications",
        "label": "Enviar email y SMS",
        "description": "Enviar emails y SMS transaccionales desde la instancia; consume saldo.",
        "sideEffect": true
      },
      {
        "scope": "catalog:read",
        "area": "catalog",
        "label": "Leer catálogo",
        "description": "Consultar servicios, tarifas y conceptos del tarifario.",
        "sideEffect": false
      },
      {
        "scope": "stock:read",
        "area": "stock",
        "label": "Leer stock",
        "description": "Consultar existencias y referencias de recambios.",
        "sideEffect": false
      },
      {
        "scope": "stock:write",
        "area": "stock",
        "label": "Ajustar stock",
        "description": "Dar entradas y salidas de recambios.",
        "sideEffect": true
      },
      {
        "scope": "webhooks:manage",
        "area": "webhooks",
        "label": "Gestionar webhooks",
        "description": "Crear, listar y borrar las suscripciones a eventos de esta clave.",
        "sideEffect": true
      },
      {
        "scope": "reports:read",
        "area": "reports",
        "label": "Leer informes",
        "description": "Consultar cifras agregadas de facturación, actividad y rendimiento.",
        "sideEffect": 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": "La aceptación del presupuesto tiene que hacerla el cliente desde su enlace de seguimiento firmado."
      },
      {
        "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."
      }
    ]
  },
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyAuth": []
    }
  ]
}
