{
  "openapi": "3.1.0",
  "info": {
    "title": "API pública de Programa Taller",
    "version": "1.0.0",
    "description": "API REST de la instància del taller. Autenticació per clau (Bearer o X-Api-Key), respostes JSON, paginació per cursor, dates ISO 8601 en UTC i imports en euros amb dos decimals.",
    "x-languages": [
      "es",
      "en",
      "ca",
      "pt",
      "fr",
      "bg"
    ],
    "x-guides": [
      {
        "id": "budget-and-appointment",
        "title": "Guia: crear un pressupost i proposar cita",
        "intro": "Flux típic d'un CRM o d'una web de reserves. Totes les peticions POST porten Idempotency-Key (un UUID nou per operació; repeteix-lo només quan reintentis la mateixa). Necessites una clau live amb clients:write, vehicles:write, budgets:write, appointments:read i appointments:write.",
        "steps": [
          {
            "title": "1. Client: crear-lo o recuperar l'existent",
            "operationId": "createClient",
            "method": "POST",
            "path": "/api/v1/clients",
            "body": "{ \"name\": \"Laura Gómez\", \"phone\": \"600111222\", \"email\": \"laura@ejemplo.com\" }",
            "note": "Amb ?on_conflict=return_existing, si el telèfon, l'email o el NIF ja existeixen reps aquest client (200) en lloc de 409."
          },
          {
            "title": "2. Vehicle del client",
            "operationId": "createVehicle",
            "method": "POST",
            "path": "/api/v1/vehicles",
            "body": "{ \"client_id\": 1204, \"plate\": \"1234KLM\", \"brand\": \"Seat\", \"model\": \"León\" }",
            "note": "Si la matrícula és d'un altre client: 409 vehicle_belongs_to_other_client (decideix el taller)."
          },
          {
            "title": "3. Pressupost amb les seves partides",
            "operationId": "createBudget",
            "method": "POST",
            "path": "/api/v1/budgets",
            "body": "{ \"client_id\": 1204, \"vehicle_id\": 871, \"lines\": [{ \"description\": \"Canvi d'oli i filtre\", \"quantity\": 1, \"unit_price\": 65 }] }",
            "note": "La resposta porta els totals calculats i tracking_url: comparteix-la amb el client perquè accepti i signi."
          },
          {
            "title": "4. Forats lliures",
            "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. Proposar 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 mode «proposar» queda status=proposed fins que el taller la confirma (rebràs appointment.confirmed per webhook). Si el forat s'ha ocupat: 409 slot_unavailable amb alternatives."
          }
        ]
      }
    ]
  },
  "servers": [
    {
      "url": "https://el-teu-taller.example",
      "description": "Instància del taller"
    }
  ],
  "tags": [
    {
      "name": "General",
      "x-area": "general"
    },
    {
      "name": "Clients",
      "x-area": "clients"
    },
    {
      "name": "Vehicles",
      "x-area": "vehicles"
    },
    {
      "name": "Pressupostos",
      "x-area": "budgets"
    },
    {
      "name": "Factures",
      "x-area": "invoices"
    },
    {
      "name": "Cites",
      "x-area": "appointments"
    },
    {
      "name": "Catàleg",
      "x-area": "catalog"
    },
    {
      "name": "Comunicacions",
      "x-area": "communications"
    },
    {
      "name": "Webhooks",
      "x-area": "webhooks"
    }
  ],
  "paths": {
    "/api/v1/ping": {
      "get": {
        "operationId": "ping",
        "tags": [
          "General"
        ],
        "summary": "Provar la connexió",
        "description": "Retorna el nom de la clau, l'entorn, els permisos i l'estat dels límits. Serveix qualsevol clau vàlida.\n\nQualsevol clau vàlida, sense cap permís concret.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workshop": {
      "get": {
        "operationId": "getWorkshop",
        "tags": [
          "General"
        ],
        "summary": "Dades públiques del taller",
        "description": "Nom, raó social, CIF, adreça, contacte, horari setmanal de l'agenda, mode de reserva de cites, festius propers i zona horària.\n\nQualsevol clau vàlida, sense cap permís concret.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "tags": [
          "General"
        ],
        "summary": "Especificació OpenAPI 3.1",
        "description": "Fitxer generat des d'aquest mateix catàleg. Sense autenticació. Admet ?lang=es|en|ca|pt|fr|bg per als textos.\n\nSense autenticació.",
        "security": [],
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Idioma de les descripcions.",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en",
                "ca",
                "pt",
                "fr",
                "bg"
              ],
              "default": "es"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta correcta",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "Document OpenAPI 3.1"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/clients": {
      "get": {
        "operationId": "listClients",
        "tags": [
          "Clients"
        ],
        "summary": "Llistar clients",
        "description": "Ordenats per id ascendent. Els clients esborrats per RGPD apareixen anonimitzats, amb erased_at informat.\n\nPermís necessari: `clients:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Cerca al nom, telèfon, email i NIF/CIF (mínim 2 caràcters).",
            "schema": {
              "type": "string",
              "example": "laura"
            }
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "description": "Relacions opcionals que cal incloure.",
            "schema": {
              "type": "string",
              "enum": [
                "vehicles"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultats per pàgina (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opac retornat a next_cursor de la pàgina anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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 opac per demanar la pàgina següent; null si no n'hi ha més",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si queden més resultats",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createClient",
        "tags": [
          "Clients"
        ],
        "summary": "Crear un client",
        "description": "Mai no fusiona amb una fitxa existent: si el NIF/CIF, l'email o el telèfon ja són en un altre client respon 409 client_exists amb existing_id (o 200 amb aquest client si passes ?on_conflict=return_existing). El telèfon es desa com a la fitxa (Espanya en 9 dígits, altres països amb prefix) i l'email en minúscules; no es corregeixen errades. Les baixes comercials queden al registre de consentiments.\n\nPermís necessari: `clients:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "parameters": [
          {
            "name": "on_conflict",
            "in": "query",
            "required": false,
            "description": "error (per defecte): 409 si ja existeix. return_existing: 200 amb el client existent.",
            "schema": {
              "type": "string",
              "enum": [
                "error",
                "return_existing"
              ],
              "default": "error"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Nom i cognoms o raó social",
                    "example": "Laura Gómez"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Telèfon principal. Espanya en 9 dígits o amb +34; altres països amb el seu prefix (+351…)",
                    "example": "600111222",
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "phone_secondary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Segon telèfon",
                    "example": null,
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email (no es corregeix: se'n valida el format)",
                    "example": "laura@ejemplo.com",
                    "format": "email"
                  },
                  "billing_email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email per a les factures, si és un altre",
                    "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 les comunicacions",
                    "example": "es"
                  },
                  "preferred_contact_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp",
                      "phone",
                      null
                    ],
                    "description": "Canal preferit",
                    "example": "whatsapp"
                  },
                  "marketing_opt_out": {
                    "type": "boolean",
                    "description": "true: no vol comunicacions comercials. Queda al registre de consentiments",
                    "example": false
                  },
                  "channel_opt_out": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "Sense publicitat per email",
                        "example": false
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "Sense publicitat per SMS",
                        "example": false
                      },
                      "whatsapp": {
                        "type": "boolean",
                        "description": "Sense publicitat per WhatsApp",
                        "example": false
                      },
                      "call": {
                        "type": "boolean",
                        "description": "Sense trucades comercials",
                        "example": false
                      }
                    },
                    "required": [],
                    "additionalProperties": false,
                    "description": "Baixes comercials per canal (els avisos de servei no canvien)"
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "description": "Alta de client"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Resposta 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ó no és vàlida. | idempotency_key_required: Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — client_exists: Ja existeix un client amb aquest telèfon, email o NIF/CIF. | idempotency_conflict: Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició. | idempotency_in_progress: Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/clients/{id}": {
      "get": {
        "operationId": "getClient",
        "tags": [
          "Clients"
        ],
        "summary": "Detall d'un client",
        "description": "Permís necessari: `clients:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del client.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "description": "Relacions opcionals que cal incloure.",
            "schema": {
              "type": "string",
              "enum": [
                "vehicles"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateClient",
        "tags": [
          "Clients"
        ],
        "summary": "Modificar un client",
        "description": "Només canvien els camps enviats (null buida el camp). Es pot canviar l'email o el telèfon, i queda el valor anterior → nou a l'activitat del client amb el nom de la clau; emet client.updated. No es pot posar l'email, el telèfon o el NIF d'UNA ALTRA fitxa (409 client_exists). Les fitxes esborrades per RGPD responen 409 client_erased.\n\nPermís necessari: `clients:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del client.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Nom i cognoms o raó social",
                    "example": "Laura Gómez Ruiz"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Telèfon principal. Espanya en 9 dígits o amb +34; altres països amb el seu prefix (+351…)",
                    "example": "600111222",
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "phone_secondary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Segon telèfon",
                    "example": null,
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email (no es corregeix: se'n valida el format)",
                    "example": "laura@ejemplo.com",
                    "format": "email"
                  },
                  "billing_email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email per a les factures, si és un altre",
                    "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 les comunicacions",
                    "example": "es"
                  },
                  "preferred_contact_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp",
                      "phone",
                      null
                    ],
                    "description": "Canal preferit",
                    "example": "whatsapp"
                  },
                  "marketing_opt_out": {
                    "type": "boolean",
                    "description": "true: no vol comunicacions comercials. Queda al registre de consentiments",
                    "example": false
                  },
                  "channel_opt_out": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "Sense publicitat per email",
                        "example": false
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "Sense publicitat per SMS",
                        "example": false
                      },
                      "whatsapp": {
                        "type": "boolean",
                        "description": "Sense publicitat per WhatsApp",
                        "example": false
                      },
                      "call": {
                        "type": "boolean",
                        "description": "Sense trucades comercials",
                        "example": false
                      }
                    },
                    "required": [],
                    "additionalProperties": false,
                    "description": "Baixes comercials per canal (els avisos de servei no canvien)"
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Canvis parcials: només els camps enviats. null buida el camp"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — client_exists: Ja existeix un client amb aquest telèfon, email o NIF/CIF. | client_erased: El client va demanar l'esborrat de les seves dades (RGPD): la fitxa no admet canvis ni altes associades.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vehicles": {
      "get": {
        "operationId": "listVehicles",
        "tags": [
          "Vehicles"
        ],
        "summary": "Llistar vehicles",
        "description": "Inclou l'últim quilometratge anotat i el venciment de la ITV.\n\nPermís necessari: `vehicles:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "parameters": [
          {
            "name": "plate",
            "in": "query",
            "required": false,
            "description": "Matrícula exacta (s'ignoren espais i guions).",
            "schema": {
              "type": "string",
              "example": "1234KLM"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filtrar per client.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Estat del vehicle.",
            "schema": {
              "type": "string",
              "enum": [
                "activo",
                "baja_temporal",
                "baja"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultats per pàgina (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opac retornat a next_cursor de la pàgina anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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 opac per demanar la pàgina següent; null si no n'hi ha més",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si queden més resultats",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createVehicle",
        "tags": [
          "Vehicles"
        ],
        "summary": "Donar d'alta un vehicle",
        "description": "Sempre a nom d'un client existent (client_id). La matrícula es normalitza (majúscules, sense espais ni guions). Si ja és a la fitxa d'UN ALTRE client: 409 vehicle_belongs_to_other_client. Si el mateix client ja la té: 409 vehicle_exists amb existing_id (o 200 amb ?on_conflict=return_existing).\n\nPermís necessari: `vehicles:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "parameters": [
          {
            "name": "on_conflict",
            "in": "query",
            "required": false,
            "description": "error (per defecte) o return_existing.",
            "schema": {
              "type": "string",
              "enum": [
                "error",
                "return_existing"
              ],
              "default": "error"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Client titular (ha d'existir)",
                    "example": 1204
                  },
                  "plate": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 15,
                    "description": "Matrícula; es desa en majúscules sense espais ni guions",
                    "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 vehicle d'un client"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Resposta 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ó no és vàlida. | idempotency_key_required: Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_exists: Aquest client ja té un vehicle amb aquesta matrícula. | vehicle_belongs_to_other_client: Aquesta matrícula ja està donada d'alta a nom d'un altre client. | client_erased: El client va demanar l'esborrat de les seves dades (RGPD): la fitxa no admet canvis ni altes associades. | idempotency_conflict: Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició. | idempotency_in_progress: Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vehicles/{id}": {
      "get": {
        "operationId": "getVehicle",
        "tags": [
          "Vehicles"
        ],
        "summary": "Detall d'un vehicle",
        "description": "Permís necessari: `vehicles:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del vehicle.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateVehicle",
        "tags": [
          "Vehicles"
        ],
        "summary": "Modificar un vehicle",
        "description": "Canvis parcials. client_id no admet null: un vehicle mai no es desvincula del titular per API (sí que pot passar a un altre client existent).\n\nPermís necessari: `vehicles:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del vehicle.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Client titular (ha d'existir)",
                    "example": 1204
                  },
                  "plate": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 15,
                    "description": "Matrícula; es desa en majúscules sense espais ni guions",
                    "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": "Canvis parcials del vehicle"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_exists: Aquest client ja té un vehicle amb aquesta matrícula. | vehicle_belongs_to_other_client: Aquesta matrícula ja està donada d'alta a nom d'un altre client. | client_erased: El client va demanar l'esborrat de les seves dades (RGPD): la fitxa no admet canvis ni altes associades.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets": {
      "get": {
        "operationId": "listBudgets",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Llistar pressupostos",
        "description": "Mai no inclou els pressupostos d'ús intern del taller ni dades de cost. El detall (amb partides i totals) és a /budgets/{id}.\n\nPermís necessari: `budgets:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Estat exacte, amb el valor en castellà (Pendiente, Enviado, Aprobado, Finalizado, Facturado, Rechazado…).",
            "schema": {
              "type": "string",
              "example": "Aprobado"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filtrar per client.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "vehicle_id",
            "in": "query",
            "required": false,
            "description": "Filtrar per vehicle.",
            "schema": {
              "type": "integer",
              "example": 871
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Creats a partir d'aquesta data.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Creats fins a aquesta data.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "description": "Només registres modificats a partir d'aquesta data (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01T00:00:00Z"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultats per pàgina (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opac retornat a next_cursor de la pàgina anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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 opac per demanar la pàgina següent; null si no n'hi ha més",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si queden més resultats",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createBudget",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Crear un pressupost",
        "description": "Entra a «Pendiente» amb canal «API», igual que una alta des del programa (registre, fita d'obertura, webhook lead.created). Els totals es calculen al servidor amb l'impost del taller; si una partida no porta tax_rate, s'usa el del taller. El vehicle, si s'indica, ha de ser del client. No admet categories d'ús intern. Amb notify_client=true (i permís communications:send) s'envia al client l'acusament amb el seu enllaç de seguiment.\n\nPermís necessari: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Client (ha d'existir)",
                    "example": 1204
                  },
                  "vehicle_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Vehicle del client",
                    "example": 871
                  },
                  "category_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Categoria de servei (GET /catalog/services)",
                    "example": 5
                  },
                  "subcategory_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Subcategoria d'aquesta categoria",
                    "example": 51
                  },
                  "client_reference": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "description": "Referència del client (comanda, sinistre…) que veurà a la factura",
                    "example": "PED-2026-118"
                  },
                  "public_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000,
                    "description": "Observacions visibles per al client",
                    "example": "Revisar també el soroll de la suspensió."
                  },
                  "lines": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "object",
                      "properties": {
                        "description": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2000,
                          "description": "Concepte visible per al client",
                          "example": "Canvi d'oli i filtre"
                        },
                        "quantity": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100000,
                          "description": "Quantitat (hores en mà d'obra)",
                          "example": 1
                        },
                        "unit_price": {
                          "type": "number",
                          "minimum": -1000000,
                          "maximum": 1000000,
                          "description": "Preu unitari de venda sense impostos, en euros",
                          "example": 65
                        },
                        "tax_rate": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "minimum": 0,
                          "maximum": 30,
                          "description": "Tipus impositiu; si s'omet, el del taller (IVA/IGIC/IPSI segons la zona)",
                          "example": 21
                        },
                        "discount_pct": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "description": "Descompte 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": "Referència de recanvi",
                          "example": null
                        },
                        "group_title": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "maxLength": 200,
                          "description": "Títol del grup al qual pertany",
                          "example": null
                        }
                      },
                      "required": [
                        "description",
                        "quantity",
                        "unit_price"
                      ],
                      "additionalProperties": false,
                      "description": "Partida nova"
                    },
                    "description": "Partides inicials; els totals els calcula el servidor"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Enviar al client l'acusament amb el seu enllaç de seguiment. També exigeix communications:send",
                    "example": false
                  }
                },
                "required": [
                  "client_id"
                ],
                "additionalProperties": false,
                "description": "Pressupost nou; entra en estat «Pendiente» amb canal «API»"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Resposta 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ó no és vàlida. | idempotency_key_required: Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_belongs_to_other_client: Aquesta matrícula ja està donada d'alta a nom d'un altre client. | client_erased: El client va demanar l'esborrat de les seves dades (RGPD): la fitxa no admet canvis ni altes associades. | idempotency_conflict: Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició. | idempotency_in_progress: Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}": {
      "get": {
        "operationId": "getBudget",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Detall d'un pressupost",
        "description": "Partides (descripció, quantitat, preu unitari, impost, tipus, descompte), totals amb desglossament, cita, dates d'entrada i sortida, responsable i URL pública de seguiment.\n\nPermís necessari: `budgets:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del pressupost.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/lines": {
      "post": {
        "operationId": "addBudgetLine",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Afegir una partida",
        "description": "La resta de partides conserven el seu id. Deixa instantània prèvia i registre com qualsevol edició del programa. 409 budget_locked si el pressupost és Facturado, Facturado externamente, Cancelado, Rechazado, Desistido o ja té factura.\n\nPermís necessari: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del pressupost.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Concepte visible per al client",
                    "example": "Canvi d'oli i filtre"
                  },
                  "quantity": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "description": "Quantitat (hores en mà d'obra)",
                    "example": 1
                  },
                  "unit_price": {
                    "type": "number",
                    "minimum": -1000000,
                    "maximum": 1000000,
                    "description": "Preu unitari de venda sense impostos, en euros",
                    "example": 65
                  },
                  "tax_rate": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 30,
                    "description": "Tipus impositiu; si s'omet, el del taller (IVA/IGIC/IPSI segons la zona)",
                    "example": 21
                  },
                  "discount_pct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Descompte 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": "Referència de recanvi",
                    "example": null
                  },
                  "group_title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Títol del grup al qual pertany",
                    "example": null
                  }
                },
                "required": [
                  "description",
                  "quantity",
                  "unit_price"
                ],
                "additionalProperties": false,
                "description": "Partida nova"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Resposta 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 i pressupost amb els totals recalculats",
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/BudgetLine"
                    },
                    "budget": {
                      "$ref": "#/components/schemas/BudgetDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida. | idempotency_key_required: Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El pressupost està tancat i ja no admet aquest canvi. | idempotency_conflict: Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició. | idempotency_in_progress: Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/lines/{lineId}": {
      "patch": {
        "operationId": "updateBudgetLine",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Modificar una partida",
        "description": "Permís necessari: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del pressupost.",
            "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": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Concepte visible per al client",
                    "example": "Canvi d'oli i filtre"
                  },
                  "quantity": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "description": "Quantitat (hores en mà d'obra)",
                    "example": 1
                  },
                  "unit_price": {
                    "type": "number",
                    "minimum": -1000000,
                    "maximum": 1000000,
                    "description": "Preu unitari de venda sense impostos, en euros",
                    "example": 65
                  },
                  "tax_rate": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 30,
                    "description": "Tipus impositiu; si s'omet, el del taller (IVA/IGIC/IPSI segons la zona)",
                    "example": 21
                  },
                  "discount_pct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Descompte 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": "Referència de recanvi",
                    "example": null
                  },
                  "group_title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Títol del grup al qual pertany",
                    "example": null
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Canvis parcials d'una partida"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resposta 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 i pressupost recalculat",
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/BudgetLine"
                    },
                    "budget": {
                      "$ref": "#/components/schemas/BudgetDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El pressupost està tancat i ja no admet aquest canvi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteBudgetLine",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Treure una partida",
        "description": "Retorna el pressupost amb els totals recalculats. La partida queda a la instantània prèvia de l'historial de versions.\n\nPermís necessari: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del pressupost.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "required": true,
            "description": "Id de la partida.",
            "schema": {
              "type": "integer",
              "example": 5501
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: El pressupost està tancat i ja no admet aquest canvi.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/status": {
      "post": {
        "operationId": "changeBudgetStatus",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Canviar l'estat",
        "description": "Admet Pendiente/En cotización/Enviado (abans de l'acceptació), En curso (pressupost ja acceptat), Finalizado (des d'Aprobado, En curso o En espera) i Cancelado. «Aprobado» respon 403 client_acceptance_required amb la tracking_url: l'acceptació la signa el client. Facturar, rebutjar o desistir responen 403 status_transition_forbidden; des de Facturado o Cancelado, 409 budget_locked. «Enviado» exigeix a més communications:send i sent_via: registra que el TEU sistema ja l'ha enviat (no l'envia). Cap canvi no avisa el client excepte Finalizado amb notify_client=true i communications:send.\n\nPermís necessari: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del pressupost.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Només amb status=Enviado: canal pel qual el TEU sistema ha enviat el pressupost. Exigeix communications:send",
                    "example": "email"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Amb Finalizado: avisar el client que el vehicle és a punt (segons els avisos configurats). Exigeix communications:send",
                    "example": false
                  }
                },
                "required": [
                  "status"
                ],
                "additionalProperties": false,
                "description": "Canvi d'estat"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida. | idempotency_key_required: Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live. | client_acceptance_required: L'acceptació del pressupost l'ha de fer el client des del seu enllaç de seguiment signat. | status_transition_forbidden: Aquest canvi d'estat no està disponible per API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — invalid_status_transition: El pressupost no pot passar a aquest estat des de l'estat actual. | budget_locked: El pressupost està tancat i ja no admet aquest canvi. | idempotency_conflict: Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició. | idempotency_in_progress: Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/documents": {
      "post": {
        "operationId": "uploadBudgetDocument",
        "tags": [
          "Pressupostos"
        ],
        "summary": "Adjuntar un document",
        "description": "multipart/form-data amb el camp «file» (JPEG, PNG, WebP o PDF, comprovat pel contingut; màxim 4 MB). Per defecte només el veu el taller; client_visible=true el mostra a l'enllaç de seguiment i mechanic_visible=true a l'app del mecànic. L'empremta d'idempotència inclou el fitxer.\n\nPermís necessari: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Id del pressupost.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Resposta 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ó no és vàlida. | idempotency_key_required: Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — idempotency_conflict: Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició. | idempotency_in_progress: Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Error — payload_too_large: El fitxer supera la mida màxima (4 MB).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Error — unsupported_media_type: Tipus de fitxer no admès: només JPEG, PNG, WebP o PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices": {
      "get": {
        "operationId": "listInvoices",
        "tags": [
          "Factures"
        ],
        "summary": "Llistar factures",
        "description": "Factures emeses i esborranys, ordenades per id. Inclou el resum de cobraments.\n\nPermís necessari: `invoices:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Data de factura des de.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Data de factura fins a.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filtrar per client.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Estat de la factura.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "issued",
                "cancelled"
              ]
            }
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "description": "Sèrie exacta.",
            "schema": {
              "type": "string",
              "example": "F26"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Resultats per pàgina (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Cursor opac retornat a next_cursor de la pàgina anterior.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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 opac per demanar la pàgina següent; null si no n'hi ha més",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true si queden més resultats",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}": {
      "get": {
        "operationId": "getInvoice",
        "tags": [
          "Factures"
        ],
        "summary": "Detall d'una factura",
        "description": "Permís necessari: `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": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/pdf": {
      "get": {
        "operationId": "getInvoicePdf",
        "tags": [
          "Factures"
        ],
        "summary": "PDF d'una factura",
        "description": "El mateix PDF que genera el programa (amb QR Verifactu si la factura està emesa). Resposta application/pdf.\n\nPermís necessari: `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": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments": {
      "get": {
        "operationId": "listAppointments",
        "tags": [
          "Cites"
        ],
        "summary": "Cites confirmades i proposades",
        "description": "Per defecte els propers 30 dies. status=confirmed són cites fixades a l'agenda; status=proposed són propostes del client pendents que el taller confirmi (bloquegen el forat).\n\nPermís necessari: `appointments:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Inici del rang (per defecte ara).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Fi del rang (per defecte +30 dies, màxim 1 any).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Només un tipus.",
            "schema": {
              "type": "string",
              "enum": [
                "confirmed",
                "proposed"
              ]
            }
          },
          {
            "name": "box_id",
            "in": "query",
            "required": false,
            "description": "Filtrar per elevador/box.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAppointment",
        "tags": [
          "Cites"
        ],
        "summary": "Proposar o reservar una cita",
        "description": "Segueix el mode de reserva del taller (booking_mode a GET /workshop). En «propose» es crea una proposta (status=proposed) que bloqueja el forat fins que el taller la confirma; demanar mode=book respon 403 booking_mode_propose_only. En «book» la cita queda ferma (status=confirmed), tret que demanis mode=propose. El forat es valida amb la mateixa lògica que /appointments/availability; si no és lliure, 409 slot_unavailable amb fins a 3 alternatives a error.alternatives. No s'avisa el client tret de notify_client=true amb communications:send.\n\nPermís necessari: `appointments:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Clau única per operació (es recomana un UUID). Repetir-la amb el mateix cos en 24 h retorna la resposta desada amb Idempotent-Replay: true; amb un altre cos, 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": "Pressupost al qual pertany la cita",
                    "example": 1234
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Inici amb zona horària (ISO 8601)",
                    "example": "2026-10-14T09:00:00+02:00"
                  },
                  "box_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Elevador/box; si s'omet, el primer lliure",
                    "example": 2
                  },
                  "duration_minutes": {
                    "type": "integer",
                    "minimum": 15,
                    "maximum": 720,
                    "description": "Durada; per defecte la mínima de l'agenda",
                    "example": 60
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "propose",
                      "book"
                    ],
                    "description": "propose: proposta que el taller confirma. book: cita ferma (només si el taller treballa en mode «reservar»). Per defecte, el mode del taller",
                    "example": "propose"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Enviar la confirmació de cita al client (només cites fermes). Exigeix communications:send",
                    "example": false
                  }
                },
                "required": [
                  "budget_id",
                  "start"
                ],
                "additionalProperties": false,
                "description": "Cita o proposta de cita"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Resposta 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ó no és vàlida. | idempotency_key_required: Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live. | booking_mode_propose_only: El taller treballa en mode «proposar cita»: només es poden crear propostes que el taller confirma.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — slot_unavailable: Aquest forat no està disponible. | budget_locked: El pressupost està tancat i ja no admet aquest canvi. | idempotency_conflict: Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició. | idempotency_in_progress: Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: El cos de la petició no és vàlid: revisa la llista «fields».",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments/availability": {
      "get": {
        "operationId": "getAvailability",
        "tags": [
          "Cites"
        ],
        "summary": "Forats lliures",
        "description": "Mateixa lògica que l'agenda i la web de seguiment: horari del taller i de cada box, dinar, festius nacionals, autonòmics i locals, cites obertes i propostes pendents (que bloquegen el seu forat). Inicis cada 30 min i com a mínim 1 h des d'ara. Per defecte els propers 7 dies (màxim 31). Inclou el booking_mode del taller.\n\nPermís necessari: `appointments:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Des de (per defecte ara).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Fins a (per defecte +7 dies; màxim 31 dies).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "duration_minutes",
            "in": "query",
            "required": false,
            "description": "Durada de la cita (15–720). Per defecte la mínima de l'agenda.",
            "schema": {
              "type": "integer",
              "example": 60
            }
          },
          {
            "name": "box_id",
            "in": "query",
            "required": false,
            "description": "Només aquest box.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Màxim de forats (1–500).",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments/{id}": {
      "delete": {
        "operationId": "cancelAppointment",
        "tags": [
          "Cites"
        ],
        "summary": "Anul·lar una cita o retirar una proposta",
        "description": "b<pressupost>: anul·la la cita confirmada (igual que «Cancel·lar cita» a la fitxa). p<proposta>: retira la proposta pendent; amb notify_client=true i communications:send es convida el client a triar una altra hora. Emet appointment.cancelled.\n\nPermís necessari: `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": "Només propostes: avisar el client.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/catalog/services": {
      "get": {
        "operationId": "listServices",
        "tags": [
          "Catàleg"
        ],
        "summary": "Categories i subcategories de servei",
        "description": "Permís necessari: `catalog:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/catalog/rates": {
      "get": {
        "operationId": "listRates",
        "tags": [
          "Catàleg"
        ],
        "summary": "Tarifes de mà d'obra",
        "description": "Només el preu de venda per hora; el cost mai no surt per l'API.\n\nPermís necessari: `catalog:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/communications": {
      "get": {
        "operationId": "listCommunications",
        "tags": [
          "Comunicacions"
        ],
        "summary": "Registre de comunicacions",
        "description": "Últimes comunicacions (email, SMS, WhatsApp, trucades, push) amb el contingut complet, de la més recent a la més antiga.\n\nPermís necessari: `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àxim de resultats (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/logs": {
      "get": {
        "operationId": "listLogs",
        "tags": [
          "Comunicacions"
        ],
        "summary": "Registre de comunicacions (àlies antic)",
        "description": "Mateixa consulta que /communications però retorna { items }. Es manté per compatibilitat; fes servir /communications.\n\nPermís necessari: `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àxim de resultats (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/email": {
      "post": {
        "operationId": "sendEmail",
        "tags": [
          "Comunicacions"
        ],
        "summary": "Enviar un email transaccional",
        "description": "Surt amb el compte de correu configurat al taller i queda al registre de Comunicació. Si rebota, l'email del client es marca com a no vàlid.\n\nPermís necessari: `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": "El seu vehicle és a punt"
                  },
                  "text": {
                    "type": "string",
                    "example": "Ja el pot passar a recollir."
                  },
                  "html": {
                    "type": "string",
                    "example": "<p>Ja el pot passar a recollir.</p>"
                  },
                  "fromName": {
                    "type": "string",
                    "example": "Taller"
                  },
                  "replyTo": {
                    "type": "string",
                    "format": "email",
                    "example": "taller@ejemplo.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sms": {
      "post": {
        "operationId": "sendSms",
        "tags": [
          "Comunicacions"
        ],
        "summary": "Enviar un SMS transaccional",
        "description": "Surt amb el servei d'SMS configurat al taller i queda al registre de Comunicació, on s'actualitza l'estat de lliurament.\n\nPermís necessari: `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": "El seu vehicle ja es pot recollir."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "Llistar els webhooks de la clau",
        "description": "Inclou el catàleg d'esdeveniments disponibles. Mai no retorna els secrets.\n\nPermís necessari: `webhooks:manage`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "responses": {
          "200": {
            "description": "Resposta 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": "Pressupost acceptat"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: La petició no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Crear un webhook",
        "description": "El secret de signatura (whsec_…) només viatja en aquesta resposta.\n\nPermís necessari: `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": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "get": {
        "operationId": "getWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Detall d'un webhook i les darreres entregues",
        "description": "Permís necessari: `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": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Modificar un webhook",
        "description": "Permís necessari: `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": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Esborrar un webhook",
        "description": "Permís necessari: `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": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/test": {
      "post": {
        "operationId": "testWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Enviar una entrega de prova (test.ping) al webhook",
        "description": "Permís necessari: `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": "Resposta 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ó no és vàlida.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key. | invalid_api_key: La clau API no és vàlida per a aquesta instància.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: L'adreça IP d'origen no és a la llista permesa d'aquesta clau. | insufficient_scope: La clau API no té el permís necessari per a aquesta operació. | plan_scope_not_allowed: El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla. | test_key_forbidden: Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: No s'ha trobat el recurs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar. | plan_quota_exceeded: S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Error intern.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "lead.created": {
      "post": {
        "summary": "Pressupost creat",
        "description": "S'ha creat un pressupost nou (des del programa, la web, l'email o l'assistent).\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Pressupostos"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "lead.sent": {
      "post": {
        "summary": "Pressupost enviat",
        "description": "El pressupost s'ha enviat al client.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Pressupostos"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "lead.accepted": {
      "post": {
        "summary": "Pressupost acceptat",
        "description": "El client o el taller han aprovat el pressupost.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Pressupostos"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "lead.rejected": {
      "post": {
        "summary": "Pressupost rebutjat",
        "description": "El pressupost s'ha rebutjat, amb el motiu si n'hi ha.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Pressupostos"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "lead.status_changed": {
      "post": {
        "summary": "Canvi d'estat",
        "description": "Qualsevol canvi d'estat del pressupost (inclou els anteriors).\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Pressupostos"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "appointment.proposed": {
      "post": {
        "summary": "Cita proposada",
        "description": "Un client proposa una cita pendent que el taller la confirmi.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Cites"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "appointment.confirmed": {
      "post": {
        "summary": "Cita confirmada",
        "description": "Una cita queda confirmada a l'agenda.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Cites"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "appointment.cancelled": {
      "post": {
        "summary": "Cita cancel·lada",
        "description": "S'ha anul·lat la cita d'un pressupost.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Cites"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "vehicle.checked_in": {
      "post": {
        "summary": "Vehicle rebut",
        "description": "El vehicle ha entrat al taller (recepció o sense cita).\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Vehicles"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "vehicle.ready": {
      "post": {
        "summary": "Vehicle a punt",
        "description": "La reparació ha acabat i el vehicle es pot recollir.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Vehicles"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "vehicle.delivered": {
      "post": {
        "summary": "Vehicle lliurat",
        "description": "El client ha recollit el vehicle.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Vehicles"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "invoice.issued": {
      "post": {
        "summary": "Factura emesa",
        "description": "S'ha emès una factura amb número definitiu.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Factures"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "payment.received": {
      "post": {
        "summary": "Cobrament registrat",
        "description": "S'ha anotat un cobrament d'una factura.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Factures"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "client.created": {
      "post": {
        "summary": "Client creat",
        "description": "S'ha donat d'alta un client.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Clients"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "client.updated": {
      "post": {
        "summary": "Client actualitzat",
        "description": "S'han modificat les dades d'un client.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Clients"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "communication.inbound": {
      "post": {
        "summary": "Missatge entrant",
        "description": "Ha arribat un email, SMS, WhatsApp o trucada d'un client.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "email.sent": {
      "post": {
        "summary": "Email enviat",
        "description": "S'ha enviat un email (campanyes, avisos o API).\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "email.failed": {
      "post": {
        "summary": "Email fallit",
        "description": "Un email no s'ha pogut enviar.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "email.opened": {
      "post": {
        "summary": "Email obert",
        "description": "El destinatari ha obert l'email.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "email.clicked": {
      "post": {
        "summary": "Clic a l'email",
        "description": "El destinatari ha clicat un enllaç de l'email.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "email.unsubscribed": {
      "post": {
        "summary": "Baixa d'email",
        "description": "El destinatari s'ha donat de baixa dels emails.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "email.bounced": {
      "post": {
        "summary": "Email rebotat",
        "description": "L'email ha rebotat.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "sms.sent": {
      "post": {
        "summary": "SMS enviat",
        "description": "S'ha enviat un SMS.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "sms.failed": {
      "post": {
        "summary": "SMS fallit",
        "description": "Un SMS no s'ha pogut enviar.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "sms.unsubscribed": {
      "post": {
        "summary": "Baixa d'SMS",
        "description": "El destinatari ha demanat no rebre SMS.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "whatsapp.unsubscribed": {
      "post": {
        "summary": "Baixa de WhatsApp",
        "description": "El destinatari ha demanat no rebre WhatsApp.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    },
    "campaign.finished": {
      "post": {
        "summary": "Campanya acabada",
        "description": "Una campanya ha acabat d'enviar-se.\n\nLliurament POST signat amb X-PT-Signature (HMAC-SHA256 de \"<t>.<cos>\"). Respon 2xx en menys de 5 s.",
        "tags": [
          "Comunicacions"
        ],
        "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": "Rebut"
          }
        }
      }
    }
  },
  "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": "Codi estable de l'error",
                "example": "insufficient_scope"
              },
              "message": {
                "type": "string",
                "description": "Missatge per a persones (pot canviar)",
                "example": "La clau API no té el permís «budgets:read»."
              }
            },
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": false
          }
        },
        "description": "Format uniforme d'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 cos de la petició no és vàlid: revisa la llista «fields»."
              },
              "fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "path": {
                      "type": "string",
                      "description": "Ruta del camp (lines[0].quantity)",
                      "example": "phone"
                    },
                    "message": {
                      "type": "string",
                      "example": "El format no és vàlid."
                    }
                  },
                  "required": [
                    "path",
                    "message"
                  ],
                  "additionalProperties": false
                }
              }
            },
            "required": [
              "code",
              "message",
              "fields"
            ],
            "additionalProperties": false
          }
        },
        "description": "422: cos que no compleix l'esquema",
        "required": [
          "error"
        ],
        "additionalProperties": false
      },
      "Page": {
        "type": "object",
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Cursor opac per demanar la pàgina següent; null si no n'hi ha més",
            "example": "aWQ6MTIzNA"
          },
          "has_more": {
            "type": "boolean",
            "description": "true si queden més resultats",
            "example": true
          }
        },
        "description": "Camps de paginació que acompanyen `data` a tots els llistats",
        "required": [
          "next_cursor",
          "has_more"
        ],
        "additionalProperties": false
      },
      "StaffRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id de l'usuari del taller",
            "example": 3
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nom visible",
            "example": "Marta"
          }
        },
        "description": "Persona del taller: només id i nom",
        "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èfon principal en format internacional quan se sap",
            "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 les comunicacions (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 vol comunicacions comercials",
            "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": "Canals que el client ha demanat no fer servir",
            "required": [
              "email",
              "sms",
              "whatsapp",
              "call"
            ],
            "additionalProperties": false
          },
          "erased_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Data de la sol·licitud d'esborrat RGPD; les dades personals ja estan anonimitzades",
            "example": null
          },
          "vehicles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Vehicle"
            },
            "description": "Només amb ?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": "Últim quilometratge anotat en un pressupost",
            "example": 84500
          },
          "itv_expiry_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Venciment 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": "Canvi d'oli i filtre"
          },
          "extended_detail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Detall ampliat visible per al client",
            "example": null
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Referència de recanvi",
            "example": null
          },
          "line_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "labor, part, diagnosis, other…",
            "example": "labor"
          },
          "group_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Títol del grup de partides",
            "example": null
          },
          "quantity": {
            "type": "number",
            "example": 1
          },
          "unit_price": {
            "type": "number",
            "description": "Preu unitari de venda sense impostos",
            "example": 65
          },
          "discount_pct": {
            "type": "number",
            "description": "Descompte en percentatge",
            "example": 0
          },
          "tax_rate": {
            "type": "number",
            "description": "Tipus impositiu aplicat (IVA/IGIC)",
            "example": 21
          },
          "tax_exempt_code": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "price_estimated": {
            "type": "boolean",
            "description": "Preu pendent de confirmar",
            "example": false
          },
          "base": {
            "type": "number",
            "description": "Base de la línia amb el descompte aplicat",
            "example": 65
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "sort_order": {
            "type": "integer",
            "example": 0
          }
        },
        "description": "Partida de pressupost (mai no inclou el cost)",
        "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": "Import en euros amb dos decimals",
            "example": 65
          },
          "tax": {
            "type": "number",
            "description": "Import en euros amb dos decimals",
            "example": 13.65
          },
          "total": {
            "type": "number",
            "description": "Import en euros amb dos decimals",
            "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": "Import en euros amb dos decimals",
                  "example": 65
                },
                "quota": {
                  "type": "number",
                  "description": "Import en euros amb dos decimals",
                  "example": 13.65
                }
              },
              "required": [
                "rate",
                "base",
                "quota"
              ],
              "additionalProperties": false
            }
          }
        },
        "description": "Totals amb desglossament per tipus impositiu",
        "required": [
          "base",
          "tax",
          "total",
          "currency",
          "tax_rates"
        ],
        "additionalProperties": false
      },
      "Appointment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador de la cita: b<pressupost> si està confirmada, p<proposta> si està pendent",
            "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": "Quan va confirmar el client (null si només la va fixar el taller)",
            "example": null
          },
          "budget_status": {
            "type": [
              "string",
              "null"
            ],
            "example": "Aprobado"
          },
          "checked_in_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Entrada real del vehicle",
            "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 pel qual va arribar la proposta (web, portal, voice…)",
            "example": null
          },
          "proposed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Quan es va proposar (només status=proposed)",
            "example": null
          }
        },
        "description": "Cita confirmada o proposta pendent",
        "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 (valors en castellà)",
            "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èfon, assistent, 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 vehicle al taller",
            "example": null
          },
          "date_out": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Sortida del vehicle",
            "example": null
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "example": 84500
          },
          "client_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Referència que el client vol veure a 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": "Signatura del client en acceptar",
            "example": null
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Lliurament del vehicle al client",
            "example": null
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "tracking_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL pública de seguiment signada",
            "example": "https://taller.ejemplo.com/public/seguimiento?id=…"
          }
        },
        "description": "Pressupost (resum)",
        "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": "Observacions visibles per al client",
            "example": "Revisar també el soroll a la suspensió."
          },
          "totals": {
            "$ref": "#/components/schemas/Totals"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BudgetLine"
            }
          }
        },
        "description": "Pressupost amb partides i totals",
        "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": "Canvi d'oli i filtre"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "group_title": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "quantity": {
            "type": "number",
            "example": 1
          },
          "unit_price": {
            "type": "number",
            "description": "Import en euros amb dos decimals",
            "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": "Import en euros amb dos decimals",
            "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": "Import en euros amb dos decimals",
            "example": 78.65
          },
          "pending": {
            "type": "number",
            "description": "Import en euros amb dos decimals",
            "example": 0
          },
          "settled": {
            "type": "boolean",
            "example": true
          }
        },
        "description": "Resum de cobraments",
        "required": [
          "paid",
          "pending",
          "settled"
        ],
        "additionalProperties": false
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 412
          },
          "number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Número dins de la sèrie",
            "example": 87
          },
          "series": {
            "type": [
              "string",
              "null"
            ],
            "example": "F26"
          },
          "full_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sèrie + 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": "Import en euros amb dos decimals",
            "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ègim especial de béns usats",
            "example": false
          },
          "verifactu_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Empremta Verifactu de la factura emesa",
            "example": "3f9a…"
          }
        },
        "description": "Factura (resum)",
        "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": "Import en euros amb dos decimals",
            "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": "Dades fiscals tal com van quedar a 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": "Import en euros amb dos decimals",
                  "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 amb línies i cobraments",
        "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": "Preu orientatiu de venda, si el taller l'ha fixat",
            "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": "Canvi d'oli"
                },
                "reference_price": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "example": 65
                },
                "currency": {
                  "type": "string",
                  "enum": [
                    "EUR"
                  ],
                  "example": "EUR"
                }
              },
              "required": [
                "id",
                "name",
                "reference_price",
                "currency"
              ],
              "additionalProperties": false
            }
          }
        },
        "description": "Categoria de servei amb les subcategories",
        "required": [
          "id",
          "name",
          "reference_price",
          "currency",
          "subcategories"
        ],
        "additionalProperties": false
      },
      "LaborRate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "name": {
            "type": "string",
            "example": "Mà d'obra general"
          },
          "price_per_hour": {
            "type": [
              "number",
              "null"
            ],
            "description": "Preu de venda per hora sense impostos",
            "example": 48
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "is_default": {
            "type": "boolean",
            "example": true
          }
        },
        "description": "Tarifa de mà d'obra (només preu de venda)",
        "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 client proposa i el taller confirma; book: el client reserva directament",
            "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": "Horari setmanal de l'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": "Festius dels propers 90 dies (nacionals, autonòmics i els del taller)"
          }
        },
        "description": "Dades públiques 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": "El seu pressupost"
          },
          "body": {
            "type": [
              "string",
              "null"
            ],
            "description": "Contingut complet",
            "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": "Segons (només trucades)",
            "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ó 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 instància (host)",
            "example": "taller.ejemplo.com"
          },
          "server_time": {
            "type": "string",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "version": {
            "type": "string",
            "description": "Versió de l'API",
            "example": "v1"
          }
        },
        "description": "Estat de la clau",
        "required": [
          "ok",
          "key",
          "rate_limit",
          "instance",
          "server_time",
          "version"
        ],
        "additionalProperties": false
      },
      "Slot": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "description": "Inici (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": "Forat lliure",
        "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": "Forats lliures amb la mateixa lògica que l'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 anul·lada; withdrawn: proposta 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 per al client al seu enllaç de seguiment",
            "example": false
          },
          "mechanic_visible": {
            "type": "boolean",
            "description": "Visible a l'app del mecànic",
            "example": false
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          }
        },
        "description": "Document adjunt a un pressupost",
        "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": "Nom de l'esdeveniment",
            "example": "lead.accepted"
          },
          "timestamp": {
            "type": "string",
            "example": "2026-10-06T10:15:00.000Z"
          },
          "data": {
            "type": "object",
            "description": "Càrrega específica de l'esdeveniment",
            "additionalProperties": true,
            "example": {
              "leadId": 1234,
              "from": "Enviado",
              "to": "Aprobado"
            }
          }
        },
        "description": "Cos de tot lliurament 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": "Subscripció a esdeveniments (mai no inclou el secret)",
        "required": [
          "id",
          "url",
          "events",
          "description",
          "active",
          "created_at"
        ],
        "additionalProperties": false
      }
    },
    "x-scopes": [
      {
        "scope": "clients:read",
        "area": "clients",
        "label": "Llegir clients",
        "description": "Consultar fitxes de clients i les seves dades de contacte.",
        "sideEffect": false
      },
      {
        "scope": "clients:write",
        "area": "clients",
        "label": "Crear i editar clients",
        "description": "Donar d'alta clients nous i modificar els existents.",
        "sideEffect": true
      },
      {
        "scope": "vehicles:read",
        "area": "vehicles",
        "label": "Llegir vehicles",
        "description": "Consultar vehicles, matrícules i el seu historial.",
        "sideEffect": false
      },
      {
        "scope": "vehicles:write",
        "area": "vehicles",
        "label": "Crear i editar vehicles",
        "description": "Donar d'alta vehicles i modificar-ne les dades.",
        "sideEffect": true
      },
      {
        "scope": "budgets:read",
        "area": "budgets",
        "label": "Llegir pressupostos",
        "description": "Consultar pressupostos, les seves partides i el seu estat.",
        "sideEffect": false
      },
      {
        "scope": "budgets:write",
        "area": "budgets",
        "label": "Crear i editar pressupostos",
        "description": "Crear pressupostos, afegir partides i canviar-ne l'estat.",
        "sideEffect": true
      },
      {
        "scope": "invoices:read",
        "area": "invoices",
        "label": "Llegir factures",
        "description": "Consultar factures emeses, imports i cobraments. Emetre factures no està disponible per API.",
        "sideEffect": false
      },
      {
        "scope": "appointments:read",
        "area": "appointments",
        "label": "Llegir cites",
        "description": "Consultar l'agenda de cites i els forats disponibles.",
        "sideEffect": false
      },
      {
        "scope": "appointments:write",
        "area": "appointments",
        "label": "Reservar i cancel·lar cites",
        "description": "Crear, moure i cancel·lar cites a l'agenda.",
        "sideEffect": true
      },
      {
        "scope": "communications:read",
        "area": "communications",
        "label": "Llegir comunicacions",
        "description": "Consultar el registre d'emails, SMS, WhatsApp i trucades (inclou el contingut complet).",
        "sideEffect": false
      },
      {
        "scope": "communications:send",
        "area": "communications",
        "label": "Enviar email i SMS",
        "description": "Enviar emails i SMS transaccionals des de la instància; consumeix saldo.",
        "sideEffect": true
      },
      {
        "scope": "catalog:read",
        "area": "catalog",
        "label": "Llegir catàleg",
        "description": "Consultar serveis, tarifes i conceptes del tarifari.",
        "sideEffect": false
      },
      {
        "scope": "stock:read",
        "area": "stock",
        "label": "Llegir estoc",
        "description": "Consultar existències i referències de recanvis.",
        "sideEffect": false
      },
      {
        "scope": "stock:write",
        "area": "stock",
        "label": "Ajustar estoc",
        "description": "Donar entrades i sortides de recanvis.",
        "sideEffect": true
      },
      {
        "scope": "webhooks:manage",
        "area": "webhooks",
        "label": "Gestionar webhooks",
        "description": "Crear, llistar i esborrar les subscripcions a esdeveniments d'aquesta clau.",
        "sideEffect": true
      },
      {
        "scope": "reports:read",
        "area": "reports",
        "label": "Llegir informes",
        "description": "Consultar xifres agregades de facturació, activitat i rendiment.",
        "sideEffect": false
      }
    ],
    "x-error-codes": [
      {
        "code": "missing_api_key",
        "status": 401,
        "message": "Falta la clau API: envia-la a la capçalera Authorization: Bearer pt_… o X-Api-Key."
      },
      {
        "code": "invalid_api_key",
        "status": 401,
        "message": "La clau API no és vàlida per a aquesta instància."
      },
      {
        "code": "revoked_api_key",
        "status": 401,
        "message": "La clau API està revocada."
      },
      {
        "code": "expired_api_key",
        "status": 401,
        "message": "La clau API ha caducat."
      },
      {
        "code": "ip_not_allowed",
        "status": 403,
        "message": "L'adreça IP d'origen no és a la llista permesa d'aquesta clau."
      },
      {
        "code": "insufficient_scope",
        "status": 403,
        "message": "La clau API no té el permís necessari per a aquesta operació."
      },
      {
        "code": "test_key_forbidden",
        "status": 403,
        "message": "Una clau de prova (pt_test_) no pot fer operacions amb efectes: fes servir una clau live."
      },
      {
        "code": "rate_limited",
        "status": 429,
        "message": "Has superat el límit de peticions d'aquesta clau. Espera i torna-ho a provar."
      },
      {
        "code": "instance_rate_limited",
        "status": 503,
        "message": "La instància està rebent massa peticions per API en aquest moment. Torna-ho a provar d'aquí a uns segons."
      },
      {
        "code": "plan_quota_exceeded",
        "status": 429,
        "message": "S'ha esgotat la quota diària de peticions del pla d'API del taller. Es renova a les 00:00 UTC; per a més volum, millora el pla."
      },
      {
        "code": "plan_scope_not_allowed",
        "status": 403,
        "message": "El pla d'API del taller no inclou aquest permís. Per fer-lo servir cal millorar el pla."
      },
      {
        "code": "bad_request",
        "status": 400,
        "message": "La petició no és vàlida."
      },
      {
        "code": "not_found",
        "status": 404,
        "message": "No s'ha trobat el recurs."
      },
      {
        "code": "upstream_error",
        "status": 502,
        "message": "Un servei extern ha rebutjat l'operació."
      },
      {
        "code": "internal_error",
        "status": 500,
        "message": "Error intern."
      },
      {
        "code": "validation_error",
        "status": 422,
        "message": "El cos de la petició no és vàlid: revisa la llista «fields»."
      },
      {
        "code": "idempotency_key_required",
        "status": 400,
        "message": "Falta la capçalera Idempotency-Key (obligatòria a tot POST; de 8 a 255 caràcters visibles)."
      },
      {
        "code": "idempotency_conflict",
        "status": 409,
        "message": "Aquesta Idempotency-Key ja s'ha fet servir les últimes 24 h amb una altra petició."
      },
      {
        "code": "idempotency_in_progress",
        "status": 409,
        "message": "Hi ha una altra petició amb la mateixa Idempotency-Key en curs. Torna-ho a provar d'aquí a uns segons."
      },
      {
        "code": "client_exists",
        "status": 409,
        "message": "Ja existeix un client amb aquest telèfon, email o NIF/CIF."
      },
      {
        "code": "client_erased",
        "status": 409,
        "message": "El client va demanar l'esborrat de les seves dades (RGPD): la fitxa no admet canvis ni altes associades."
      },
      {
        "code": "vehicle_exists",
        "status": 409,
        "message": "Aquest client ja té un vehicle amb aquesta matrícula."
      },
      {
        "code": "vehicle_belongs_to_other_client",
        "status": 409,
        "message": "Aquesta matrícula ja està donada d'alta a nom d'un altre client."
      },
      {
        "code": "budget_locked",
        "status": 409,
        "message": "El pressupost està tancat i ja no admet aquest canvi."
      },
      {
        "code": "invalid_status_transition",
        "status": 409,
        "message": "El pressupost no pot passar a aquest estat des de l'estat actual."
      },
      {
        "code": "status_transition_forbidden",
        "status": 403,
        "message": "Aquest canvi d'estat no està disponible per API."
      },
      {
        "code": "client_acceptance_required",
        "status": 403,
        "message": "L'acceptació del pressupost l'ha de fer el client des del seu enllaç de seguiment signat."
      },
      {
        "code": "booking_mode_propose_only",
        "status": 403,
        "message": "El taller treballa en mode «proposar cita»: només es poden crear propostes que el taller confirma."
      },
      {
        "code": "slot_unavailable",
        "status": 409,
        "message": "Aquest forat no està disponible."
      },
      {
        "code": "payload_too_large",
        "status": 413,
        "message": "El fitxer supera la mida màxima (4 MB)."
      },
      {
        "code": "unsupported_media_type",
        "status": 415,
        "message": "Tipus de fitxer no admès: només JPEG, PNG, WebP o PDF."
      }
    ]
  },
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyAuth": []
    }
  ]
}
