{
  "openapi": "3.1.0",
  "info": {
    "title": "Promec public API",
    "version": "1.0.0",
    "description": "REST API of the workshop instance. Key authentication (Bearer or X-Api-Key), JSON responses, cursor pagination, ISO 8601 UTC dates and EUR amounts with two decimals.",
    "x-languages": [
      "es",
      "en",
      "ca",
      "pt",
      "fr",
      "bg"
    ],
    "x-guides": [
      {
        "id": "budget-and-appointment",
        "title": "Guide: create a budget and propose an appointment",
        "intro": "Typical flow for a CRM or booking site. Every POST carries an Idempotency-Key (a fresh UUID per operation; reuse it only when retrying the same one). You need a live key with clients:write, vehicles:write, budgets:write, appointments:read and appointments:write.",
        "steps": [
          {
            "title": "1. Client: create it or get the existing one",
            "operationId": "createClient",
            "method": "POST",
            "path": "/api/v1/clients",
            "body": "{ \"name\": \"Laura Gómez\", \"phone\": \"600111222\", \"email\": \"laura@ejemplo.com\" }",
            "note": "With ?on_conflict=return_existing, if the phone, email or tax id exist you get that client (200) instead of 409."
          },
          {
            "title": "2. The client's vehicle",
            "operationId": "createVehicle",
            "method": "POST",
            "path": "/api/v1/vehicles",
            "body": "{ \"client_id\": 1204, \"plate\": \"1234KLM\", \"brand\": \"Seat\", \"model\": \"León\" }",
            "note": "If the plate belongs to another client: 409 vehicle_belongs_to_other_client (the workshop decides)."
          },
          {
            "title": "3. Budget with its lines",
            "operationId": "createBudget",
            "method": "POST",
            "path": "/api/v1/budgets",
            "body": "{ \"client_id\": 1204, \"vehicle_id\": 871, \"lines\": [{ \"description\": \"Oil and filter change\", \"quantity\": 1, \"unit_price\": 65 }] }",
            "note": "The response includes the computed totals and tracking_url: share it with the client so they can accept and sign."
          },
          {
            "title": "4. Free slots",
            "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. Propose the appointment",
            "operationId": "createAppointment",
            "method": "POST",
            "path": "/api/v1/appointments",
            "body": "{ \"budget_id\": 1234, \"start\": \"2026-10-14T09:00:00+02:00\", \"duration_minutes\": 60 }",
            "note": "In «propose» mode it stays status=proposed until the workshop confirms it (you get appointment.confirmed by webhook). If the slot was taken: 409 slot_unavailable with alternatives."
          }
        ]
      }
    ]
  },
  "servers": [
    {
      "url": "https://your-shop.example",
      "description": "Workshop instance"
    }
  ],
  "tags": [
    {
      "name": "General",
      "x-area": "general"
    },
    {
      "name": "Clients",
      "x-area": "clients"
    },
    {
      "name": "Vehicles",
      "x-area": "vehicles"
    },
    {
      "name": "Budgets",
      "x-area": "budgets"
    },
    {
      "name": "Invoices",
      "x-area": "invoices"
    },
    {
      "name": "Appointments",
      "x-area": "appointments"
    },
    {
      "name": "Catalog",
      "x-area": "catalog"
    },
    {
      "name": "Communications",
      "x-area": "communications"
    },
    {
      "name": "Webhooks",
      "x-area": "webhooks"
    }
  ],
  "paths": {
    "/api/v1/ping": {
      "get": {
        "operationId": "ping",
        "tags": [
          "General"
        ],
        "summary": "Test the connection",
        "description": "Returns the key name, environment, scopes and rate-limit state. Any valid key works.\n\nAny valid key, no specific scope.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/workshop": {
      "get": {
        "operationId": "getWorkshop",
        "tags": [
          "General"
        ],
        "summary": "Workshop public data",
        "description": "Name, legal name, tax id, address, contact details, weekly schedule, appointment booking mode, upcoming holidays and timezone.\n\nAny valid key, no specific scope.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "tags": [
          "General"
        ],
        "summary": "OpenAPI 3.1 specification",
        "description": "Generated from this very catalog. No authentication. Accepts ?lang=es|en|ca|pt|fr|bg for the descriptions.\n\nNo authentication.",
        "security": [],
        "parameters": [
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language of the descriptions.",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en",
                "ca",
                "pt",
                "fr",
                "bg"
              ],
              "default": "es"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "description": "OpenAPI 3.1 document"
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/clients": {
      "get": {
        "operationId": "listClients",
        "tags": [
          "Clients"
        ],
        "summary": "List clients",
        "description": "Ordered by ascending id. Clients erased under GDPR appear anonymised, with erased_at set.\n\nRequired scope: `clients:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Searches name, phone, email and tax id (2 characters minimum).",
            "schema": {
              "type": "string",
              "example": "laura"
            }
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "description": "Optional relations to include.",
            "schema": {
              "type": "string",
              "enum": [
                "vehicles"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Results per page (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor returned as next_cursor by the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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": "Opaque cursor to request the next page; null when there are no more",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true when more results remain",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createClient",
        "tags": [
          "Clients"
        ],
        "summary": "Create a client",
        "description": "Never merges into an existing record: if the tax id, email or phone already belong to another client it answers 409 client_exists with existing_id (or 200 with that client when ?on_conflict=return_existing). Phones are stored like in the app (Spain as 9 digits, other countries with prefix), emails lower-cased; typos are not auto-corrected. Marketing opt-outs are recorded in the consent log.\n\nRequired scope: `clients:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "parameters": [
          {
            "name": "on_conflict",
            "in": "query",
            "required": false,
            "description": "error (default): 409 when it exists. return_existing: 200 with the existing client.",
            "schema": {
              "type": "string",
              "enum": [
                "error",
                "return_existing"
              ],
              "default": "error"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Full name or company name",
                    "example": "Laura Gómez"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Main phone. Spain as 9 digits or with +34; other countries with their prefix (+351…)",
                    "example": "600111222",
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "phone_secondary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Second phone",
                    "example": null,
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email (not corrected: only the format is validated)",
                    "example": "laura@ejemplo.com",
                    "format": "email"
                  },
                  "billing_email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Invoicing email, if different",
                    "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": "Language for communications",
                    "example": "es"
                  },
                  "preferred_contact_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp",
                      "phone",
                      null
                    ],
                    "description": "Preferred channel",
                    "example": "whatsapp"
                  },
                  "marketing_opt_out": {
                    "type": "boolean",
                    "description": "true: does not want marketing messages. Recorded in the consent log",
                    "example": false
                  },
                  "channel_opt_out": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "No marketing by email",
                        "example": false
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "No marketing by SMS",
                        "example": false
                      },
                      "whatsapp": {
                        "type": "boolean",
                        "description": "No marketing by WhatsApp",
                        "example": false
                      },
                      "call": {
                        "type": "boolean",
                        "description": "No sales calls",
                        "example": false
                      }
                    },
                    "required": [],
                    "additionalProperties": false,
                    "description": "Marketing opt-outs per channel (service notices are unaffected)"
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false,
                "description": "New client"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful response",
            "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: The request is not valid. | idempotency_key_required: Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — client_exists: A client with that phone, email or tax id already exists. | idempotency_conflict: That Idempotency-Key was already used in the last 24 h with a different request. | idempotency_in_progress: Another request with the same Idempotency-Key is in progress. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/clients/{id}": {
      "get": {
        "operationId": "getClient",
        "tags": [
          "Clients"
        ],
        "summary": "Client detail",
        "description": "Required scope: `clients:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Client id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "expand",
            "in": "query",
            "required": false,
            "description": "Optional relations to include.",
            "schema": {
              "type": "string",
              "enum": [
                "vehicles"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateClient",
        "tags": [
          "Clients"
        ],
        "summary": "Update a client",
        "description": "Only the fields sent change (null clears a field). Changing email or phone is allowed and records old → new in the client's activity with the key name; emits client.updated. Setting another client's email, phone or tax id answers 409 client_exists. GDPR-erased clients answer 409 client_erased.\n\nRequired scope: `clients:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "clients:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Client id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Full name or company name",
                    "example": "Laura Gómez Ruiz"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Main phone. Spain as 9 digits or with +34; other countries with their prefix (+351…)",
                    "example": "600111222",
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "phone_secondary": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 30,
                    "description": "Second phone",
                    "example": null,
                    "pattern": "^[0-9+()\\s.\\-]{6,30}$"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Email (not corrected: only the format is validated)",
                    "example": "laura@ejemplo.com",
                    "format": "email"
                  },
                  "billing_email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 254,
                    "description": "Invoicing email, if different",
                    "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": "Language for communications",
                    "example": "es"
                  },
                  "preferred_contact_method": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp",
                      "phone",
                      null
                    ],
                    "description": "Preferred channel",
                    "example": "whatsapp"
                  },
                  "marketing_opt_out": {
                    "type": "boolean",
                    "description": "true: does not want marketing messages. Recorded in the consent log",
                    "example": false
                  },
                  "channel_opt_out": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "boolean",
                        "description": "No marketing by email",
                        "example": false
                      },
                      "sms": {
                        "type": "boolean",
                        "description": "No marketing by SMS",
                        "example": false
                      },
                      "whatsapp": {
                        "type": "boolean",
                        "description": "No marketing by WhatsApp",
                        "example": false
                      },
                      "call": {
                        "type": "boolean",
                        "description": "No sales calls",
                        "example": false
                      }
                    },
                    "required": [],
                    "additionalProperties": false,
                    "description": "Marketing opt-outs per channel (service notices are unaffected)"
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Partial changes: only the fields sent. null clears the field"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — client_exists: A client with that phone, email or tax id already exists. | client_erased: The client requested erasure of their data (GDPR): the record accepts no changes or linked records.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vehicles": {
      "get": {
        "operationId": "listVehicles",
        "tags": [
          "Vehicles"
        ],
        "summary": "List vehicles",
        "description": "Includes the latest recorded mileage and the MOT (ITV) expiry date.\n\nRequired scope: `vehicles:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "parameters": [
          {
            "name": "plate",
            "in": "query",
            "required": false,
            "description": "Exact plate (spaces and dashes ignored).",
            "schema": {
              "type": "string",
              "example": "1234KLM"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filter by client.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Vehicle status.",
            "schema": {
              "type": "string",
              "enum": [
                "activo",
                "baja_temporal",
                "baja"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Results per page (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor returned as next_cursor by the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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": "Opaque cursor to request the next page; null when there are no more",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true when more results remain",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createVehicle",
        "tags": [
          "Vehicles"
        ],
        "summary": "Create a vehicle",
        "description": "Always for an existing client (client_id). The plate is normalised (upper case, no spaces or dashes). If it belongs to ANOTHER client: 409 vehicle_belongs_to_other_client. If the same client already has it: 409 vehicle_exists with existing_id (or 200 with ?on_conflict=return_existing).\n\nRequired scope: `vehicles:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "parameters": [
          {
            "name": "on_conflict",
            "in": "query",
            "required": false,
            "description": "error (default) or return_existing.",
            "schema": {
              "type": "string",
              "enum": [
                "error",
                "return_existing"
              ],
              "default": "error"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Owner client (must exist)",
                    "example": 1204
                  },
                  "plate": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 15,
                    "description": "Plate; stored upper-case without spaces or dashes",
                    "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": "Engine size (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": "Spanish DGT environmental label (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": "New vehicle for a client"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful response",
            "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: The request is not valid. | idempotency_key_required: Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_exists: That client already has a vehicle with that plate. | vehicle_belongs_to_other_client: That plate is already registered to another client. | client_erased: The client requested erasure of their data (GDPR): the record accepts no changes or linked records. | idempotency_conflict: That Idempotency-Key was already used in the last 24 h with a different request. | idempotency_in_progress: Another request with the same Idempotency-Key is in progress. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vehicles/{id}": {
      "get": {
        "operationId": "getVehicle",
        "tags": [
          "Vehicles"
        ],
        "summary": "Vehicle detail",
        "description": "Required scope: `vehicles:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Vehicle id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateVehicle",
        "tags": [
          "Vehicles"
        ],
        "summary": "Update a vehicle",
        "description": "Partial changes. client_id does not accept null: a vehicle is never unlinked from its owner via the API (it can move to another existing client).\n\nRequired scope: `vehicles:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "vehicles:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Vehicle id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Owner client (must exist)",
                    "example": 1204
                  },
                  "plate": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 15,
                    "description": "Plate; stored upper-case without spaces or dashes",
                    "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": "Engine size (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": "Spanish DGT environmental label (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": "Partial vehicle changes"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_exists: That client already has a vehicle with that plate. | vehicle_belongs_to_other_client: That plate is already registered to another client. | client_erased: The client requested erasure of their data (GDPR): the record accepts no changes or linked records.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets": {
      "get": {
        "operationId": "listBudgets",
        "tags": [
          "Budgets"
        ],
        "summary": "List budgets",
        "description": "Never includes the workshop's internal budgets nor cost data. Lines and totals are in /budgets/{id}.\n\nRequired scope: `budgets:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Exact status (Pendiente, Enviado, Aprobado, Finalizado, Facturado, Rechazado…).",
            "schema": {
              "type": "string",
              "example": "Aprobado"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filter by client.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "vehicle_id",
            "in": "query",
            "required": false,
            "description": "Filter by vehicle.",
            "schema": {
              "type": "integer",
              "example": 871
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Created on or after this date.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Created on or before this date.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "updated_since",
            "in": "query",
            "required": false,
            "description": "Only records modified on or after this date (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01T00:00:00Z"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Results per page (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor returned as next_cursor by the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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": "Opaque cursor to request the next page; null when there are no more",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true when more results remain",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createBudget",
        "tags": [
          "Budgets"
        ],
        "summary": "Create a budget",
        "description": "Starts as «Pendiente» with channel «API», just like a budget created in the app (log, opening milestone, lead.created webhook). Totals are computed server-side with the workshop's tax; lines without tax_rate use the workshop rate. The vehicle, if given, must belong to the client. Internal-use categories are not allowed. With notify_client=true (and communications:send) the client gets the acknowledgement with their tracking link.\n\nRequired scope: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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 (must exist)",
                    "example": 1204
                  },
                  "vehicle_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "The client's vehicle",
                    "example": 871
                  },
                  "category_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Service category (GET /catalog/services)",
                    "example": 5
                  },
                  "subcategory_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 1,
                    "description": "Subcategory of that category",
                    "example": 51
                  },
                  "client_reference": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 100,
                    "description": "Client reference (order, insurance claim…) shown on the invoice",
                    "example": "PED-2026-118"
                  },
                  "public_notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 5000,
                    "description": "Notes visible to the client",
                    "example": "Also check the suspension noise."
                  },
                  "lines": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "object",
                      "properties": {
                        "description": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2000,
                          "description": "Line text visible to the client",
                          "example": "Oil and filter change"
                        },
                        "quantity": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100000,
                          "description": "Quantity (hours for labour)",
                          "example": 1
                        },
                        "unit_price": {
                          "type": "number",
                          "minimum": -1000000,
                          "maximum": 1000000,
                          "description": "Unit sale price before tax, in euros",
                          "example": 65
                        },
                        "tax_rate": {
                          "type": [
                            "number",
                            "null"
                          ],
                          "minimum": 0,
                          "maximum": 30,
                          "description": "Tax rate; if omitted, the workshop's (VAT/IGIC/IPSI depending on its region)",
                          "example": 21
                        },
                        "discount_pct": {
                          "type": "number",
                          "minimum": 0,
                          "maximum": 100,
                          "description": "Discount in %",
                          "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": "Part number",
                          "example": null
                        },
                        "group_title": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "maxLength": 200,
                          "description": "Title of the group it belongs to",
                          "example": null
                        }
                      },
                      "required": [
                        "description",
                        "quantity",
                        "unit_price"
                      ],
                      "additionalProperties": false,
                      "description": "New line"
                    },
                    "description": "Initial lines; totals are calculated by the server"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Send the client the acknowledgement with their tracking link. Also requires communications:send",
                    "example": false
                  }
                },
                "required": [
                  "client_id"
                ],
                "additionalProperties": false,
                "description": "New budget; enters status «Pendiente» with channel «API»"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful response",
            "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: The request is not valid. | idempotency_key_required: Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — vehicle_belongs_to_other_client: That plate is already registered to another client. | client_erased: The client requested erasure of their data (GDPR): the record accepts no changes or linked records. | idempotency_conflict: That Idempotency-Key was already used in the last 24 h with a different request. | idempotency_in_progress: Another request with the same Idempotency-Key is in progress. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}": {
      "get": {
        "operationId": "getBudget",
        "tags": [
          "Budgets"
        ],
        "summary": "Budget detail",
        "description": "Lines (description, quantity, unit price, tax, type, discount), totals with breakdown, appointment, in/out dates, assigned user and public tracking URL.\n\nRequired scope: `budgets:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Budget id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/lines": {
      "post": {
        "operationId": "addBudgetLine",
        "tags": [
          "Budgets"
        ],
        "summary": "Add a line",
        "description": "The other lines keep their ids. Takes a prior snapshot and logs the change like any edit in the app. 409 budget_locked when the budget is invoiced, cancelled, rejected, withdrawn or already has an invoice.\n\nRequired scope: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Budget id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Line text visible to the client",
                    "example": "Oil and filter change"
                  },
                  "quantity": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "description": "Quantity (hours for labour)",
                    "example": 1
                  },
                  "unit_price": {
                    "type": "number",
                    "minimum": -1000000,
                    "maximum": 1000000,
                    "description": "Unit sale price before tax, in euros",
                    "example": 65
                  },
                  "tax_rate": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 30,
                    "description": "Tax rate; if omitted, the workshop's (VAT/IGIC/IPSI depending on its region)",
                    "example": 21
                  },
                  "discount_pct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Discount in %",
                    "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": "Part number",
                    "example": null
                  },
                  "group_title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Title of the group it belongs to",
                    "example": null
                  }
                },
                "required": [
                  "description",
                  "quantity",
                  "unit_price"
                ],
                "additionalProperties": false,
                "description": "New line"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful response",
            "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": "Line created and budget with recalculated totals",
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/BudgetLine"
                    },
                    "budget": {
                      "$ref": "#/components/schemas/BudgetDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid. | idempotency_key_required: Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: The budget is closed and no longer accepts this change. | idempotency_conflict: That Idempotency-Key was already used in the last 24 h with a different request. | idempotency_in_progress: Another request with the same Idempotency-Key is in progress. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/lines/{lineId}": {
      "patch": {
        "operationId": "updateBudgetLine",
        "tags": [
          "Budgets"
        ],
        "summary": "Update a line",
        "description": "Required scope: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Budget id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "required": true,
            "description": "Line id.",
            "schema": {
              "type": "integer",
              "example": 5501
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Line text visible to the client",
                    "example": "Oil and filter change"
                  },
                  "quantity": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100000,
                    "description": "Quantity (hours for labour)",
                    "example": 1
                  },
                  "unit_price": {
                    "type": "number",
                    "minimum": -1000000,
                    "maximum": 1000000,
                    "description": "Unit sale price before tax, in euros",
                    "example": 65
                  },
                  "tax_rate": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 30,
                    "description": "Tax rate; if omitted, the workshop's (VAT/IGIC/IPSI depending on its region)",
                    "example": 21
                  },
                  "discount_pct": {
                    "type": "number",
                    "minimum": 0,
                    "maximum": 100,
                    "description": "Discount in %",
                    "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": "Part number",
                    "example": null
                  },
                  "group_title": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "maxLength": 200,
                    "description": "Title of the group it belongs to",
                    "example": null
                  }
                },
                "required": [],
                "additionalProperties": false,
                "description": "Partial line changes"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "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": "Line updated and budget recalculated",
                  "properties": {
                    "line": {
                      "$ref": "#/components/schemas/BudgetLine"
                    },
                    "budget": {
                      "$ref": "#/components/schemas/BudgetDetail"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: The budget is closed and no longer accepts this change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteBudgetLine",
        "tags": [
          "Budgets"
        ],
        "summary": "Delete a line",
        "description": "Returns the budget with recalculated totals. The line remains in the prior snapshot of the version history.\n\nRequired scope: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Budget id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "required": true,
            "description": "Line id.",
            "schema": {
              "type": "integer",
              "example": 5501
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — budget_locked: The budget is closed and no longer accepts this change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/status": {
      "post": {
        "operationId": "changeBudgetStatus",
        "tags": [
          "Budgets"
        ],
        "summary": "Change the status",
        "description": "Accepts Pendiente/En cotización/Enviado (before acceptance), En curso (already accepted), Finalizado (from Aprobado, En curso or En espera) and Cancelado. «Aprobado» answers 403 client_acceptance_required with the tracking_url: acceptance is signed by the client. Invoicing, rejecting or withdrawing answer 403 status_transition_forbidden; from Facturado or Cancelado, 409 budget_locked. «Enviado» also requires communications:send and sent_via: it records that YOUR system already sent it (it does not send). No change notifies the client except Finalizado with notify_client=true and communications:send.\n\nRequired scope: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Budget id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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 or Cancelado",
                    "example": "En curso"
                  },
                  "sent_via": {
                    "type": "string",
                    "enum": [
                      "email",
                      "sms",
                      "whatsapp"
                    ],
                    "description": "Only with status=Enviado: channel through which YOUR system sent the budget. Requires communications:send",
                    "example": "email"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "With Finalizado: tell the client the vehicle is ready (according to the configured notices). Requires communications:send",
                    "example": false
                  }
                },
                "required": [
                  "status"
                ],
                "additionalProperties": false,
                "description": "Status change"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid. | idempotency_key_required: Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key. | client_acceptance_required: Budget acceptance must be done by the client from their signed tracking link. | status_transition_forbidden: That status change is not available through the API.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — invalid_status_transition: The budget cannot move to that status from its current one. | budget_locked: The budget is closed and no longer accepts this change. | idempotency_conflict: That Idempotency-Key was already used in the last 24 h with a different request. | idempotency_in_progress: Another request with the same Idempotency-Key is in progress. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/budgets/{id}/documents": {
      "post": {
        "operationId": "uploadBudgetDocument",
        "tags": [
          "Budgets"
        ],
        "summary": "Attach a document",
        "description": "multipart/form-data with the «file» field (JPEG, PNG, WebP or PDF, checked by content; 4 MB max). Staff-only by default; client_visible=true shows it on the tracking page and mechanic_visible=true in the mechanic app. The idempotency fingerprint includes the file.\n\nRequired scope: `budgets:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "budgets:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Budget id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Successful response",
            "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: The request is not valid. | idempotency_key_required: Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — idempotency_conflict: That Idempotency-Key was already used in the last 24 h with a different request. | idempotency_in_progress: Another request with the same Idempotency-Key is in progress. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Error — payload_too_large: The file exceeds the maximum size (4 MB).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Error — unsupported_media_type: Unsupported file type: only JPEG, PNG, WebP or PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices": {
      "get": {
        "operationId": "listInvoices",
        "tags": [
          "Invoices"
        ],
        "summary": "List invoices",
        "description": "Issued invoices and drafts ordered by id, with a payments summary.\n\nRequired scope: `invoices:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Invoice date from.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Invoice date to.",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "client_id",
            "in": "query",
            "required": false,
            "description": "Filter by client.",
            "schema": {
              "type": "integer",
              "example": 1204
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Invoice status.",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "issued",
                "cancelled"
              ]
            }
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "description": "Exact series.",
            "schema": {
              "type": "string",
              "example": "F26"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Results per page (1–200).",
            "schema": {
              "type": "integer",
              "default": 50,
              "example": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor returned as next_cursor by the previous page.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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": "Opaque cursor to request the next page; null when there are no more",
                      "example": "aWQ6MTIzNA"
                    },
                    "has_more": {
                      "type": "boolean",
                      "description": "true when more results remain",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}": {
      "get": {
        "operationId": "getInvoice",
        "tags": [
          "Invoices"
        ],
        "summary": "Invoice detail",
        "description": "Required scope: `invoices:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Invoice id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/pdf": {
      "get": {
        "operationId": "getInvoicePdf",
        "tags": [
          "Invoices"
        ],
        "summary": "Invoice PDF",
        "description": "The same PDF the program generates (with the Verifactu QR when issued). application/pdf response.\n\nRequired scope: `invoices:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "invoices:read",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Invoice id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments": {
      "get": {
        "operationId": "listAppointments",
        "tags": [
          "Appointments"
        ],
        "summary": "Confirmed and proposed appointments",
        "description": "Defaults to the next 30 days. status=confirmed are booked appointments; status=proposed are client proposals awaiting workshop confirmation (they block the slot).\n\nRequired scope: `appointments:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Range start (defaults to now).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Range end (defaults to +30 days, 1 year max).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only one kind.",
            "schema": {
              "type": "string",
              "enum": [
                "confirmed",
                "proposed"
              ]
            }
          },
          {
            "name": "box_id",
            "in": "query",
            "required": false,
            "description": "Filter by bay/box.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAppointment",
        "tags": [
          "Appointments"
        ],
        "summary": "Propose or book an appointment",
        "description": "Follows the workshop booking mode (booking_mode in GET /workshop). In «propose» a proposal is created (status=proposed) that blocks the slot until the workshop confirms it; asking mode=book answers 403 booking_mode_propose_only. In «book» the appointment is confirmed (status=confirmed) unless you ask mode=propose. The slot is checked with the same logic as /appointments/availability; if not free, 409 slot_unavailable with up to 3 alternatives in error.alternatives. The client is not notified unless notify_client=true with communications:send.\n\nRequired scope: `appointments:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "Unique key per operation (UUID recommended). Repeating it with the same body within 24 h returns the stored response with Idempotent-Replay: true; with a different body, 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": "Budget the appointment belongs to",
                    "example": 1234
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Start with timezone (ISO 8601)",
                    "example": "2026-10-14T09:00:00+02:00"
                  },
                  "box_id": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Lift/bay; if omitted, the first free one",
                    "example": 2
                  },
                  "duration_minutes": {
                    "type": "integer",
                    "minimum": 15,
                    "maximum": 720,
                    "description": "Duration; defaults to the diary's minimum",
                    "example": 60
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "propose",
                      "book"
                    ],
                    "description": "propose: proposal the workshop confirms. book: firm appointment (only if the workshop works in «book» mode). Defaults to the workshop's mode",
                    "example": "propose"
                  },
                  "notify_client": {
                    "type": "boolean",
                    "description": "Send the appointment confirmation to the client (firm appointments only). Requires communications:send",
                    "example": false
                  }
                },
                "required": [
                  "budget_id",
                  "start"
                ],
                "additionalProperties": false,
                "description": "Appointment or appointment proposal"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Successful response",
            "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: The request is not valid. | idempotency_key_required: Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key. | booking_mode_propose_only: The workshop works in «propose appointment» mode: only proposals the workshop confirms can be created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Error — slot_unavailable: That slot is not available. | budget_locked: The budget is closed and no longer accepts this change. | idempotency_conflict: That Idempotency-Key was already used in the last 24 h with a different request. | idempotency_in_progress: Another request with the same Idempotency-Key is in progress. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Error — validation_error: The request body is not valid: check the «fields» list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments/availability": {
      "get": {
        "operationId": "getAvailability",
        "tags": [
          "Appointments"
        ],
        "summary": "Free slots",
        "description": "Same logic as the agenda and the tracking page: workshop and bay hours, lunch break, national/regional/local holidays, open appointments and pending proposals (which block their slot). Starts every 30 min, at least 1 h from now. Defaults to the next 7 days (31 max). Includes the workshop booking_mode.\n\nRequired scope: `appointments:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "From (defaults to now).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-01"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "To (defaults to +7 days; 31 days max).",
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-10-31"
            }
          },
          {
            "name": "duration_minutes",
            "in": "query",
            "required": false,
            "description": "Appointment length (15–720). Defaults to the agenda minimum.",
            "schema": {
              "type": "integer",
              "example": 60
            }
          },
          {
            "name": "box_id",
            "in": "query",
            "required": false,
            "description": "Only that bay.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum slots (1–500).",
            "schema": {
              "type": "integer",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/appointments/{id}": {
      "delete": {
        "operationId": "cancelAppointment",
        "tags": [
          "Appointments"
        ],
        "summary": "Cancel an appointment or withdraw a proposal",
        "description": "b<budget>: cancels the confirmed appointment (same as «Cancel appointment» in the app). p<proposal>: withdraws the pending proposal; with notify_client=true and communications:send the client is invited to pick another time. Emits appointment.cancelled.\n\nRequired scope: `appointments:write`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "appointments:write",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Appointment id (b1234 or p88).",
            "schema": {
              "type": "string",
              "example": "b1234"
            }
          },
          {
            "name": "notify_client",
            "in": "query",
            "required": false,
            "description": "Proposals only: notify the client.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/catalog/services": {
      "get": {
        "operationId": "listServices",
        "tags": [
          "Catalog"
        ],
        "summary": "Service categories and subcategories",
        "description": "Required scope: `catalog:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/catalog/rates": {
      "get": {
        "operationId": "listRates",
        "tags": [
          "Catalog"
        ],
        "summary": "Labor rates",
        "description": "Sale price per hour only; cost never leaves through the API.\n\nRequired scope: `catalog:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "catalog:read",
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/communications": {
      "get": {
        "operationId": "listCommunications",
        "tags": [
          "Communications"
        ],
        "summary": "Communications log",
        "description": "Latest communications (email, SMS, WhatsApp, calls, push) with full content, newest first.\n\nRequired scope: `communications:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "Channel.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "email",
                "sms",
                "whatsapp",
                "call",
                "push"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/logs": {
      "get": {
        "operationId": "listLogs",
        "tags": [
          "Communications"
        ],
        "summary": "Communications log (legacy alias)",
        "description": "Same query as /communications but returns { items }. Kept for compatibility; prefer /communications.\n\nRequired scope: `communications:read`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "communications:read",
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "Channel.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "email",
                "sms",
                "whatsapp",
                "call",
                "push"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results (1–200).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/email": {
      "post": {
        "operationId": "sendEmail",
        "tags": [
          "Communications"
        ],
        "summary": "Send a transactional email",
        "description": "Sent with the email account configured by the workshop and logged in Communications. If it bounces, the client's email is flagged as invalid.\n\nRequired scope: `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": "Your vehicle is ready"
                  },
                  "text": {
                    "type": "string",
                    "example": "You can come and collect it."
                  },
                  "html": {
                    "type": "string",
                    "example": "<p>You can come and collect it.</p>"
                  },
                  "fromName": {
                    "type": "string",
                    "example": "Taller"
                  },
                  "replyTo": {
                    "type": "string",
                    "format": "email",
                    "example": "taller@ejemplo.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sms": {
      "post": {
        "operationId": "sendSms",
        "tags": [
          "Communications"
        ],
        "summary": "Send a transactional SMS",
        "description": "Sent with the SMS service configured by the workshop and logged in Communications, where its delivery status is updated.\n\nRequired scope: `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": "Your vehicle is ready for collection."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "tags": [
          "Webhooks"
        ],
        "summary": "List this key's webhooks",
        "description": "Includes the catalog of available events. Never returns secrets.\n\nRequired scope: `webhooks:manage`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "responses": {
          "200": {
            "description": "Successful response",
            "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": "Budget accepted"
                          },
                          "description": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Error — bad_request: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Create a webhook",
        "description": "The signing secret (whsec_…) is only returned in this response.\n\nRequired scope: `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": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "get": {
        "operationId": "getWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Webhook detail and recent deliveries",
        "description": "Required scope: `webhooks:manage`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Update a webhook",
        "description": "Required scope: `webhooks:manage`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "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": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Delete a webhook",
        "description": "Required scope: `webhooks:manage`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/{id}/test": {
      "post": {
        "operationId": "testWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "Send a test delivery (test.ping) to the webhook",
        "description": "Required scope: `webhooks:manage`.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "apiKeyAuth": []
          }
        ],
        "x-scope": "webhooks:manage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Webhook id.",
            "schema": {
              "type": "integer",
              "example": 1234
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response",
            "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: The request is not valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Error — missing_api_key: Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header. | invalid_api_key: The API key is not valid for this instance.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Error — ip_not_allowed: The source IP address is not on this key's allow list. | insufficient_scope: The API key lacks the scope required for this operation. | plan_scope_not_allowed: The workshop's API plan does not include this scope. Upgrade the plan to use it. | test_key_forbidden: A test key (pt_test_) cannot perform operations with side effects: use a live key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Error — not_found: Resource not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Error — rate_limited: You have exceeded this key's request limit. Wait and retry. | plan_quota_exceeded: The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error — internal_error: Internal error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Error — instance_rate_limited: The instance is receiving too many API requests right now. Retry in a few seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "lead.created": {
      "post": {
        "summary": "Budget created",
        "description": "A new budget was created (from the app, the website, email or the assistant).\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Budgets"
        ],
        "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": "Received"
          }
        }
      }
    },
    "lead.sent": {
      "post": {
        "summary": "Budget sent",
        "description": "The budget was sent to the client.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Budgets"
        ],
        "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": "Received"
          }
        }
      }
    },
    "lead.accepted": {
      "post": {
        "summary": "Budget accepted",
        "description": "The client or the workshop approved the budget.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Budgets"
        ],
        "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": "Received"
          }
        }
      }
    },
    "lead.rejected": {
      "post": {
        "summary": "Budget rejected",
        "description": "The budget was rejected, with the reason if there is one.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Budgets"
        ],
        "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": "Received"
          }
        }
      }
    },
    "lead.status_changed": {
      "post": {
        "summary": "Status change",
        "description": "Any budget status change (includes the ones above).\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Budgets"
        ],
        "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": "Received"
          }
        }
      }
    },
    "appointment.proposed": {
      "post": {
        "summary": "Appointment proposed",
        "description": "A client proposes an appointment the workshop still has to confirm.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Appointments"
        ],
        "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": "Received"
          }
        }
      }
    },
    "appointment.confirmed": {
      "post": {
        "summary": "Appointment confirmed",
        "description": "An appointment is confirmed in the diary.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Appointments"
        ],
        "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": "Received"
          }
        }
      }
    },
    "appointment.cancelled": {
      "post": {
        "summary": "Appointment cancelled",
        "description": "A budget's appointment was cancelled.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Appointments"
        ],
        "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": "Received"
          }
        }
      }
    },
    "vehicle.checked_in": {
      "post": {
        "summary": "Vehicle checked in",
        "description": "The vehicle entered the workshop (check-in or walk-in).\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 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": "Received"
          }
        }
      }
    },
    "vehicle.ready": {
      "post": {
        "summary": "Vehicle ready",
        "description": "The repair is finished and the vehicle is ready for collection.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 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": "Received"
          }
        }
      }
    },
    "vehicle.delivered": {
      "post": {
        "summary": "Vehicle delivered",
        "description": "The client collected the vehicle.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 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": "Received"
          }
        }
      }
    },
    "invoice.issued": {
      "post": {
        "summary": "Invoice issued",
        "description": "An invoice was issued with its final number.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Invoices"
        ],
        "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": "Received"
          }
        }
      }
    },
    "payment.received": {
      "post": {
        "summary": "Payment recorded",
        "description": "A payment was recorded against an invoice.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Invoices"
        ],
        "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": "Received"
          }
        }
      }
    },
    "client.created": {
      "post": {
        "summary": "Client created",
        "description": "A client was created.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 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": "Received"
          }
        }
      }
    },
    "client.updated": {
      "post": {
        "summary": "Client updated",
        "description": "A client's details were changed.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 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": "Received"
          }
        }
      }
    },
    "communication.inbound": {
      "post": {
        "summary": "Inbound message",
        "description": "An email, SMS, WhatsApp or call arrived from a client.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "email.sent": {
      "post": {
        "summary": "Email sent",
        "description": "An email was sent (campaigns, notices or API).\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "email.failed": {
      "post": {
        "summary": "Email failed",
        "description": "An email could not be sent.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "email.opened": {
      "post": {
        "summary": "Email opened",
        "description": "The recipient opened the email.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "email.clicked": {
      "post": {
        "summary": "Email click",
        "description": "The recipient clicked a link in the email.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "email.unsubscribed": {
      "post": {
        "summary": "Email unsubscribe",
        "description": "The recipient unsubscribed from emails.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "email.bounced": {
      "post": {
        "summary": "Email bounced",
        "description": "The email bounced.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "sms.sent": {
      "post": {
        "summary": "SMS sent",
        "description": "An SMS was sent.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "sms.failed": {
      "post": {
        "summary": "SMS failed",
        "description": "An SMS could not be sent.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "sms.unsubscribed": {
      "post": {
        "summary": "SMS opt-out",
        "description": "The recipient asked not to receive SMS.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "whatsapp.unsubscribed": {
      "post": {
        "summary": "WhatsApp opt-out",
        "description": "The recipient asked not to receive WhatsApp messages.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    },
    "campaign.finished": {
      "post": {
        "summary": "Campaign finished",
        "description": "A campaign finished sending.\n\nSigned POST delivery with X-PT-Signature (HMAC-SHA256 of \"<t>.<body>\"). Respond 2xx within 5 s.",
        "tags": [
          "Communications"
        ],
        "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": "Received"
          }
        }
      }
    }
  },
  "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": "Stable error code",
                "example": "insufficient_scope"
              },
              "message": {
                "type": "string",
                "description": "Human-readable message (may change)",
                "example": "The API key lacks the «budgets:read» scope."
              }
            },
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": false
          }
        },
        "description": "Uniform error format",
        "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": "The request body is not valid: check the «fields» list."
              },
              "fields": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "path": {
                      "type": "string",
                      "description": "Field path (lines[0].quantity)",
                      "example": "phone"
                    },
                    "message": {
                      "type": "string",
                      "example": "The format is not valid."
                    }
                  },
                  "required": [
                    "path",
                    "message"
                  ],
                  "additionalProperties": false
                }
              }
            },
            "required": [
              "code",
              "message",
              "fields"
            ],
            "additionalProperties": false
          }
        },
        "description": "422: body that does not match the schema",
        "required": [
          "error"
        ],
        "additionalProperties": false
      },
      "Page": {
        "type": "object",
        "properties": {
          "next_cursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Opaque cursor to request the next page; null when there are no more",
            "example": "aWQ6MTIzNA"
          },
          "has_more": {
            "type": "boolean",
            "description": "true when more results remain",
            "example": true
          }
        },
        "description": "Pagination fields that accompany `data` in every list",
        "required": [
          "next_cursor",
          "has_more"
        ],
        "additionalProperties": false
      },
      "StaffRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Workshop user id",
            "example": 3
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Display name",
            "example": "Marta"
          }
        },
        "description": "Workshop staff member: id and name only",
        "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": "Lift 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": "Main phone in international format when known",
            "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": "Language for communications (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": "Does not want marketing messages",
            "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": "Channels the client asked not to be used",
            "required": [
              "email",
              "sms",
              "whatsapp",
              "call"
            ],
            "additionalProperties": false
          },
          "erased_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Date of the GDPR erasure request; personal data is already anonymised",
            "example": null
          },
          "vehicles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Vehicle"
            },
            "description": "Only with ?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": "VIN (chassis number)",
            "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": "Spanish DGT environmental label",
            "example": "C"
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Latest mileage recorded on a budget",
            "example": 84500
          },
          "itv_expiry_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "MOT (ITV) expiry date",
            "example": "2027-03-15"
          },
          "status": {
            "type": "string",
            "description": "activo, baja_temporal or 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": "Oil and filter change"
          },
          "extended_detail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Extended detail visible to the client",
            "example": null
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Part number",
            "example": null
          },
          "line_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "labor, part, diagnosis, other…",
            "example": "labor"
          },
          "group_title": {
            "type": [
              "string",
              "null"
            ],
            "description": "Line group title",
            "example": null
          },
          "quantity": {
            "type": "number",
            "example": 1
          },
          "unit_price": {
            "type": "number",
            "description": "Unit sale price before tax",
            "example": 65
          },
          "discount_pct": {
            "type": "number",
            "description": "Discount as a percentage",
            "example": 0
          },
          "tax_rate": {
            "type": "number",
            "description": "Tax rate applied (VAT/IGIC)",
            "example": 21
          },
          "tax_exempt_code": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "price_estimated": {
            "type": "boolean",
            "description": "Price awaiting confirmation",
            "example": false
          },
          "base": {
            "type": "number",
            "description": "Line net amount with the discount applied",
            "example": 65
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "sort_order": {
            "type": "integer",
            "example": 0
          }
        },
        "description": "Budget line (never includes 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": "Amount in euros with two decimals",
            "example": 65
          },
          "tax": {
            "type": "number",
            "description": "Amount in euros with two decimals",
            "example": 13.65
          },
          "total": {
            "type": "number",
            "description": "Amount in euros with two 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": "Amount in euros with two decimals",
                  "example": 65
                },
                "quota": {
                  "type": "number",
                  "description": "Amount in euros with two decimals",
                  "example": 13.65
                }
              },
              "required": [
                "rate",
                "base",
                "quota"
              ],
              "additionalProperties": false
            }
          }
        },
        "description": "Totals broken down by tax rate",
        "required": [
          "base",
          "tax",
          "total",
          "currency",
          "tax_rates"
        ],
        "additionalProperties": false
      },
      "Appointment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Appointment id: b<budget> when confirmed, p<proposal> when pending",
            "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": "When the client confirmed (null if only the workshop set it)",
            "example": null
          },
          "budget_status": {
            "type": [
              "string",
              "null"
            ],
            "example": "Aprobado"
          },
          "checked_in_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Actual vehicle check-in",
            "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": "Channel the proposal came through (web, portal, voice…)",
            "example": null
          },
          "proposed_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When it was proposed (status=proposed only)",
            "example": null
          }
        },
        "description": "Confirmed appointment or pending proposal",
        "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 (values kept in Spanish)",
            "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": "Source: web, email, phone, assistant, 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": "Vehicle check-in at the workshop",
            "example": null
          },
          "date_out": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Vehicle check-out",
            "example": null
          },
          "km": {
            "type": [
              "integer",
              "null"
            ],
            "example": 84500
          },
          "client_reference": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reference the client wants on the invoice",
            "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": "Client signature on acceptance",
            "example": null
          },
          "delivered_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Vehicle handover to the client",
            "example": null
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "tracking_url": {
            "type": [
              "string",
              "null"
            ],
            "description": "Signed public tracking URL",
            "example": "https://taller.ejemplo.com/public/seguimiento?id=…"
          }
        },
        "description": "Budget (summary)",
        "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": "Notes visible to the client",
            "example": "Also check the noise in the suspension."
          },
          "totals": {
            "$ref": "#/components/schemas/Totals"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BudgetLine"
            }
          }
        },
        "description": "Budget with lines and 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": "Oil and filter change"
          },
          "reference": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "group_title": {
            "type": [
              "string",
              "null"
            ],
            "example": null
          },
          "quantity": {
            "type": "number",
            "example": 1
          },
          "unit_price": {
            "type": "number",
            "description": "Amount in euros with two 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": "Amount in euros with two 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": "Amount in euros with two decimals",
            "example": 78.65
          },
          "pending": {
            "type": "number",
            "description": "Amount in euros with two decimals",
            "example": 0
          },
          "settled": {
            "type": "boolean",
            "example": true
          }
        },
        "description": "Payments summary",
        "required": [
          "paid",
          "pending",
          "settled"
        ],
        "additionalProperties": false
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 412
          },
          "number": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Number within the series",
            "example": 87
          },
          "series": {
            "type": [
              "string",
              "null"
            ],
            "example": "F26"
          },
          "full_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Series + number",
            "example": "F2687"
          },
          "kind": {
            "type": "string",
            "enum": [
              "invoice",
              "rectification",
              "simplified",
              "other"
            ],
            "example": "invoice"
          },
          "rectifies_number": {
            "type": [
              "string",
              "null"
            ],
            "description": "Number of the corrected invoice",
            "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": "Amount in euros with two 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": "Special scheme for second-hand goods",
            "example": false
          },
          "verifactu_hash": {
            "type": [
              "string",
              "null"
            ],
            "description": "Verifactu hash of the issued invoice",
            "example": "3f9a…"
          }
        },
        "description": "Invoice (summary)",
        "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": "Amount in euros with two 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": "Tax details exactly as they appear on the invoice",
            "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": "Amount in euros with two 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": "Invoice with lines and payments",
        "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": "Indicative sale price, if the workshop set one",
            "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": "Oil change"
                },
                "reference_price": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "example": 65
                },
                "currency": {
                  "type": "string",
                  "enum": [
                    "EUR"
                  ],
                  "example": "EUR"
                }
              },
              "required": [
                "id",
                "name",
                "reference_price",
                "currency"
              ],
              "additionalProperties": false
            }
          }
        },
        "description": "Service category with its subcategories",
        "required": [
          "id",
          "name",
          "reference_price",
          "currency",
          "subcategories"
        ],
        "additionalProperties": false
      },
      "LaborRate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 1
          },
          "name": {
            "type": "string",
            "example": "General labour"
          },
          "price_per_hour": {
            "type": [
              "number",
              "null"
            ],
            "description": "Hourly sale price before tax",
            "example": 48
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "example": "EUR"
          },
          "is_default": {
            "type": "boolean",
            "example": true
          }
        },
        "description": "Labour rate (sale price only)",
        "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: the client proposes and the workshop confirms; book: the client books directly",
            "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": "Weekly diary schedule",
            "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": "Holidays in the next 90 days (national, regional and the workshop's own)"
          }
        },
        "description": "Workshop public data",
        "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": "Your budget"
          },
          "body": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full content",
            "example": "Hi 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": "Seconds (calls only)",
            "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": "Logged communication",
        "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": "Instance identifier (host)",
            "example": "taller.ejemplo.com"
          },
          "server_time": {
            "type": "string",
            "example": "2026-10-06T09:30:00.000Z"
          },
          "version": {
            "type": "string",
            "description": "API version",
            "example": "v1"
          }
        },
        "description": "Key status",
        "required": [
          "ok",
          "key",
          "rate_limit",
          "instance",
          "server_time",
          "version"
        ],
        "additionalProperties": false
      },
      "Slot": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "description": "Start (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": "Free slot",
        "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": "Free slots using the same logic as the diary",
        "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: confirmed appointment cancelled; withdrawn: proposal withdrawn",
            "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 to the client on their tracking link",
            "example": false
          },
          "mechanic_visible": {
            "type": "boolean",
            "description": "Visible in the mechanic app",
            "example": false
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "example": "2026-10-06T09:30:00.000Z"
          }
        },
        "description": "Document attached to a budget",
        "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": "Event name",
            "example": "lead.accepted"
          },
          "timestamp": {
            "type": "string",
            "example": "2026-10-06T10:15:00.000Z"
          },
          "data": {
            "type": "object",
            "description": "Event-specific payload",
            "additionalProperties": true,
            "example": {
              "leadId": 1234,
              "from": "Enviado",
              "to": "Aprobado"
            }
          }
        },
        "description": "Body of every webhook delivery",
        "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": "Event subscription (never includes the secret)",
        "required": [
          "id",
          "url",
          "events",
          "description",
          "active",
          "created_at"
        ],
        "additionalProperties": false
      }
    },
    "x-scopes": [
      {
        "scope": "clients:read",
        "area": "clients",
        "label": "Read clients",
        "description": "View client records and contact details.",
        "sideEffect": false
      },
      {
        "scope": "clients:write",
        "area": "clients",
        "label": "Create and edit clients",
        "description": "Add new clients and change existing ones.",
        "sideEffect": true
      },
      {
        "scope": "vehicles:read",
        "area": "vehicles",
        "label": "Read vehicles",
        "description": "View vehicles, plates and their history.",
        "sideEffect": false
      },
      {
        "scope": "vehicles:write",
        "area": "vehicles",
        "label": "Create and edit vehicles",
        "description": "Add vehicles and change their details.",
        "sideEffect": true
      },
      {
        "scope": "budgets:read",
        "area": "budgets",
        "label": "Read budgets",
        "description": "View budgets, their lines and status.",
        "sideEffect": false
      },
      {
        "scope": "budgets:write",
        "area": "budgets",
        "label": "Create and edit budgets",
        "description": "Create budgets, add lines and change their status.",
        "sideEffect": true
      },
      {
        "scope": "invoices:read",
        "area": "invoices",
        "label": "Read invoices",
        "description": "View issued invoices, amounts and payments. Issuing invoices is not available through the API.",
        "sideEffect": false
      },
      {
        "scope": "appointments:read",
        "area": "appointments",
        "label": "Read appointments",
        "description": "View the appointment diary and available slots.",
        "sideEffect": false
      },
      {
        "scope": "appointments:write",
        "area": "appointments",
        "label": "Book and cancel appointments",
        "description": "Create, move and cancel appointments in the diary.",
        "sideEffect": true
      },
      {
        "scope": "communications:read",
        "area": "communications",
        "label": "Read communications",
        "description": "View the log of emails, SMS, WhatsApp and calls (including full content).",
        "sideEffect": false
      },
      {
        "scope": "communications:send",
        "area": "communications",
        "label": "Send email and SMS",
        "description": "Send transactional emails and SMS from the instance; uses credit.",
        "sideEffect": true
      },
      {
        "scope": "catalog:read",
        "area": "catalog",
        "label": "Read catalog",
        "description": "View services, rates and price-list items.",
        "sideEffect": false
      },
      {
        "scope": "stock:read",
        "area": "stock",
        "label": "Read stock",
        "description": "View parts stock and part numbers.",
        "sideEffect": false
      },
      {
        "scope": "stock:write",
        "area": "stock",
        "label": "Adjust stock",
        "description": "Record parts in and out.",
        "sideEffect": true
      },
      {
        "scope": "webhooks:manage",
        "area": "webhooks",
        "label": "Manage webhooks",
        "description": "Create, list and delete this key's event subscriptions.",
        "sideEffect": true
      },
      {
        "scope": "reports:read",
        "area": "reports",
        "label": "Read reports",
        "description": "View aggregated revenue, activity and performance figures.",
        "sideEffect": false
      }
    ],
    "x-error-codes": [
      {
        "code": "missing_api_key",
        "status": 401,
        "message": "Missing API key: send it in the Authorization: Bearer pt_… or X-Api-Key header."
      },
      {
        "code": "invalid_api_key",
        "status": 401,
        "message": "The API key is not valid for this instance."
      },
      {
        "code": "revoked_api_key",
        "status": 401,
        "message": "The API key has been revoked."
      },
      {
        "code": "expired_api_key",
        "status": 401,
        "message": "The API key has expired."
      },
      {
        "code": "ip_not_allowed",
        "status": 403,
        "message": "The source IP address is not on this key's allow list."
      },
      {
        "code": "insufficient_scope",
        "status": 403,
        "message": "The API key lacks the scope required for this operation."
      },
      {
        "code": "test_key_forbidden",
        "status": 403,
        "message": "A test key (pt_test_) cannot perform operations with side effects: use a live key."
      },
      {
        "code": "rate_limited",
        "status": 429,
        "message": "You have exceeded this key's request limit. Wait and retry."
      },
      {
        "code": "instance_rate_limited",
        "status": 503,
        "message": "The instance is receiving too many API requests right now. Retry in a few seconds."
      },
      {
        "code": "plan_quota_exceeded",
        "status": 429,
        "message": "The workshop's API plan daily request quota is used up. It resets at 00:00 UTC; for more volume, upgrade the plan."
      },
      {
        "code": "plan_scope_not_allowed",
        "status": 403,
        "message": "The workshop's API plan does not include this scope. Upgrade the plan to use it."
      },
      {
        "code": "bad_request",
        "status": 400,
        "message": "The request is not valid."
      },
      {
        "code": "not_found",
        "status": 404,
        "message": "Resource not found."
      },
      {
        "code": "upstream_error",
        "status": 502,
        "message": "An external service rejected the operation."
      },
      {
        "code": "internal_error",
        "status": 500,
        "message": "Internal error."
      },
      {
        "code": "validation_error",
        "status": 422,
        "message": "The request body is not valid: check the «fields» list."
      },
      {
        "code": "idempotency_key_required",
        "status": 400,
        "message": "Missing Idempotency-Key header (required on every POST; 8 to 255 visible characters)."
      },
      {
        "code": "idempotency_conflict",
        "status": 409,
        "message": "That Idempotency-Key was already used in the last 24 h with a different request."
      },
      {
        "code": "idempotency_in_progress",
        "status": 409,
        "message": "Another request with the same Idempotency-Key is in progress. Retry in a few seconds."
      },
      {
        "code": "client_exists",
        "status": 409,
        "message": "A client with that phone, email or tax id already exists."
      },
      {
        "code": "client_erased",
        "status": 409,
        "message": "The client requested erasure of their data (GDPR): the record accepts no changes or linked records."
      },
      {
        "code": "vehicle_exists",
        "status": 409,
        "message": "That client already has a vehicle with that plate."
      },
      {
        "code": "vehicle_belongs_to_other_client",
        "status": 409,
        "message": "That plate is already registered to another client."
      },
      {
        "code": "budget_locked",
        "status": 409,
        "message": "The budget is closed and no longer accepts this change."
      },
      {
        "code": "invalid_status_transition",
        "status": 409,
        "message": "The budget cannot move to that status from its current one."
      },
      {
        "code": "status_transition_forbidden",
        "status": 403,
        "message": "That status change is not available through the API."
      },
      {
        "code": "client_acceptance_required",
        "status": 403,
        "message": "Budget acceptance must be done by the client from their signed tracking link."
      },
      {
        "code": "booking_mode_propose_only",
        "status": 403,
        "message": "The workshop works in «propose appointment» mode: only proposals the workshop confirms can be created."
      },
      {
        "code": "slot_unavailable",
        "status": 409,
        "message": "That slot is not available."
      },
      {
        "code": "payload_too_large",
        "status": 413,
        "message": "The file exceeds the maximum size (4 MB)."
      },
      {
        "code": "unsupported_media_type",
        "status": 415,
        "message": "Unsupported file type: only JPEG, PNG, WebP or PDF."
      }
    ]
  },
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyAuth": []
    }
  ]
}
