{
  "openapi": "3.1.0",
  "info": {
    "title": "API pública de Talavora",
    "version": "1.3.0",
    "summary": "Registro horario, servicio de campo, facturación y mantenimiento de una organización.",
    "description": "API REST para integrar Talavora con un ERP, la nómina, un cuadro de mando o un GMAO. Documentación con ejemplos: https://talavora.com/docs/api/.\n\n- Clave de la organización en `Authorization: Bearer tlv_pub_…`; permisos (scopes) por recurso.\n- 600 peticiones por hora y clave (`429` al pasarse).\n- Listados paginados por cursor (`limit` 1–1000, `next_cursor`), orden estable.\n- Errores con `{ \"error\": \"…\" }` y, cuando lo hay, un `code` estable.\n- `/v1` es estable: solo se AÑADEN campos y rutas (ignore los campos que no conozca).\n- Dispositivos IoT / BMS (`/v1/dispositivos/…`): NO usan la clave de la organización sino la credencial propia de cada aparato (`Authorization: Bearer tlv_dev_…`), con su cupo por minuto.\n\nCambios en 1.3.0: vistas de BI (`GET /v1/bi`, `GET /v1/bi/{vista}`, permiso `bi:read`); referencias externas (`refs_externas`, `?ref=`) en activos y espacios; posición propia de los activos (`lat`, `lng`, `posicion_*`); ingesta de dispositivos (`POST /v1/dispositivos/lecturas` y `/latido`). Solo se AÑADE: nada de 1.2.0 cambia.",
    "contact": {
      "name": "Soporte de Talavora",
      "email": "soporte@talavora.com",
      "url": "https://talavora.com/docs/api/"
    }
  },
  "servers": [
    {
      "url": "https://yaladpjmdzxwnxmvepqg.supabase.co/functions/v1/api-publica",
      "description": "Producción"
    }
  ],
  "security": [
    {
      "claveApi": []
    }
  ],
  "tags": [
    {
      "name": "Utilidad",
      "description": "Comprobar la clave y el estado de la plataforma."
    },
    {
      "name": "Registro horario",
      "description": "Personas, fichajes y ausencias."
    },
    {
      "name": "Servicio de campo y facturación",
      "description": "Clientes, órdenes de trabajo con su parte, y facturas de venta."
    },
    {
      "name": "Mantenimiento",
      "description": "Espacios, activos, incidencias, preventivo, contadores y órdenes de mantenimiento. Solo con Mantenimiento activado en la organización."
    },
    {
      "name": "BI",
      "description": "Vistas de Mantenimiento de solo lectura con nombres y columnas estables, para Power BI, Tableau o Metabase, en JSON o CSV. Permiso `bi:read` dicho expresamente en la clave y función «Vistas para BI» (`mnt_bi`) activada."
    },
    {
      "name": "Dispositivos",
      "description": "Pasarelas, sensores, BMS o PLC que mandan lecturas de los contadores de Mantenimiento con SU credencial (`tlv_dev_…`), no con la clave de la organización. Función «Dispositivos conectados» (`mnt_iot`)."
    }
  ],
  "x-talavora-scopes": {
    "empleados:read": "Personas",
    "fichajes:read": "Fichajes",
    "ausencias:read": "Ausencias",
    "clientes:read": "Clientes",
    "ordenes:read": "Órdenes de trabajo y su parte",
    "facturas:read": "Facturas de venta",
    "mantenimiento:read": "Leer Mantenimiento",
    "mantenimiento:write": "Comunicar incidencias, anotar lecturas y dar de alta o cambiar activos y espacios (nunca borra)",
    "mantenimiento:costes": "Importes de Mantenimiento (coste de órdenes y materiales; coste de adquisición al escribir)",
    "bi:read": "Vistas de BI de Mantenimiento (`/v1/bi`), en JSON o CSV. Hay que pedirlo expresamente: el comodín `*` de las claves antiguas no lo abre"
  },
  "paths": {
    "/v1/me": {
      "get": {
        "tags": [
          "Utilidad"
        ],
        "operationId": "obtenerClave",
        "summary": "Organización y permisos de la clave",
        "description": "Comprueba que la clave funciona y dice de qué organización es y qué permisos tiene. No necesita ningún permiso concreto.",
        "x-talavora-scope": null,
        "responses": {
          "200": {
            "description": "Datos de la clave.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "empresa_id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "scopes": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Scope"
                          }
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        }
      }
    },
    "/v1/salud": {
      "get": {
        "tags": [
          "Utilidad"
        ],
        "operationId": "salud",
        "summary": "Estado de la plataforma (sin clave)",
        "description": "Para vigilancia externa: no necesita clave y no devuelve datos de nadie. Responde `503` si la base de datos no responde, contesta con error o tarda más de 3 s. La respuesta se reutiliza 20 s (`cacheada: true`).",
        "x-talavora-scope": null,
        "responses": {
          "200": {
            "description": "La plataforma responde con normalidad.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Salud"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "503": {
            "description": "La plataforma no está en condiciones de atender.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Salud"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          }
        },
        "security": []
      }
    },
    "/v1/empleados": {
      "get": {
        "tags": [
          "Registro horario"
        ],
        "operationId": "listarEmpleados",
        "summary": "Personas",
        "description": "Personas de la organización (de alta y de baja), por nombre e `id`. Paginado por cursor.",
        "x-talavora-scope": "empleados:read",
        "responses": {
          "200": {
            "description": "Personas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Empleado"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/empleados/{id}": {
      "get": {
        "tags": [
          "Registro horario"
        ],
        "operationId": "obtenerEmpleado",
        "summary": "Una persona",
        "description": "Con su fecha de alta y de nacimiento. `404` si no existe o es de otra organización.",
        "x-talavora-scope": "empleados:read",
        "responses": {
          "200": {
            "description": "La persona.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/EmpleadoDetalle"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/fichajes": {
      "get": {
        "tags": [
          "Registro horario"
        ],
        "operationId": "listarFichajes",
        "summary": "Marcajes del registro de jornada",
        "description": "Del más reciente al más antiguo (desempate por `id`). `desde` y `hasta` son días en UTC (00:00:00 y 23:59:59 UTC). Los marcajes anulados no salen (se archivan aparte; llega el webhook `fichaje.anulado`).",
        "x-talavora-scope": "fichajes:read",
        "responses": {
          "200": {
            "description": "Marcajes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Fichaje"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "desde",
            "in": "query",
            "required": false,
            "description": "Día inicial incluido (AAAA-MM-DD, desde las 00:00:00 UTC).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "required": false,
            "description": "Día final incluido (AAAA-MM-DD, hasta las 23:59:59 UTC).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "empleado_id",
            "in": "query",
            "required": false,
            "description": "Solo los de esta persona.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/ausencias": {
      "get": {
        "tags": [
          "Registro horario"
        ],
        "operationId": "listarAusencias",
        "summary": "Ausencias",
        "description": "Por fecha de inicio descendente (desempate por `id`). El campo `motivo` puede llevar datos sensibles: trátelo como tal.",
        "x-talavora-scope": "ausencias:read",
        "responses": {
          "200": {
            "description": "Ausencias.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Ausencia"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Estado exacto.",
            "schema": {
              "$ref": "#/components/schemas/EstadoAusencia"
            }
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/clientes": {
      "get": {
        "tags": [
          "Servicio de campo y facturación"
        ],
        "operationId": "listarClientes",
        "summary": "Clientes",
        "description": "Por nombre e `id`. Buscar por NIF es lo habitual para cruzar la ficha con la de un ERP.",
        "x-talavora-scope": "clientes:read",
        "responses": {
          "200": {
            "description": "Clientes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Cliente"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "nif",
            "in": "query",
            "required": false,
            "description": "NIF exacto.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/clientes/{id}": {
      "get": {
        "tags": [
          "Servicio de campo y facturación"
        ],
        "operationId": "obtenerCliente",
        "summary": "Un cliente",
        "description": "`404` si no existe o es de otra organización.",
        "x-talavora-scope": "clientes:read",
        "responses": {
          "200": {
            "description": "El cliente.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Cliente"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/ordenes": {
      "get": {
        "tags": [
          "Servicio de campo y facturación"
        ],
        "operationId": "listarOrdenes",
        "summary": "Órdenes de trabajo",
        "description": "Por fecha prevista descendente (las que no tienen fecha, primero) e `id`.",
        "x-talavora-scope": "ordenes:read",
        "responses": {
          "200": {
            "description": "Órdenes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Orden"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Estado exacto de la orden.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cliente_id",
            "in": "query",
            "required": false,
            "description": "Solo las de este cliente.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "required": false,
            "description": "`fecha_prevista` ≥ desde.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "required": false,
            "description": "`fecha_prevista` ≤ hasta.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/ordenes/{id}": {
      "get": {
        "tags": [
          "Servicio de campo y facturación"
        ],
        "operationId": "obtenerOrden",
        "summary": "Una orden de trabajo",
        "description": "`404` si no existe o es de otra organización.",
        "x-talavora-scope": "ordenes:read",
        "responses": {
          "200": {
            "description": "La orden.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Orden"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/ordenes/{id}/parte": {
      "get": {
        "tags": [
          "Servicio de campo y facturación"
        ],
        "operationId": "obtenerParteOrden",
        "summary": "Parte de trabajo de una orden",
        "description": "Tiempos, materiales y la firma vigente, sin la imagen (con `hash_parte` para comprobar que el parte no ha cambiado).",
        "x-talavora-scope": "ordenes:read",
        "responses": {
          "200": {
            "description": "El parte.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ParteOrden"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/facturas": {
      "get": {
        "tags": [
          "Servicio de campo y facturación"
        ],
        "operationId": "listarFacturas",
        "summary": "Facturas de venta",
        "description": "Por fecha descendente e `id`.",
        "x-talavora-scope": "facturas:read",
        "responses": {
          "200": {
            "description": "Facturas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Factura"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Estado exacto.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "required": false,
            "description": "`fecha` ≥ desde.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "required": false,
            "description": "`fecha` ≤ hasta.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/facturas/{id}": {
      "get": {
        "tags": [
          "Servicio de campo y facturación"
        ],
        "operationId": "obtenerFactura",
        "summary": "Una factura con sus líneas",
        "description": "`404` si no existe o es de otra organización.",
        "x-talavora-scope": "facturas:read",
        "responses": {
          "200": {
            "description": "La factura.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FacturaDetalle"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/espacios": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "listarEspacios",
        "summary": "Espacios (árbol de ubicaciones)",
        "description": "Por `codigo` ascendente e `id`. Cada uno con sus `refs_externas`. Con `ref`, solo los que tienen esa referencia externa EXACTA. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "Espacios.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Espacio"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "padre_id",
            "in": "query",
            "required": false,
            "description": "Solo los hijos directos de este espacio.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "raiz",
            "in": "query",
            "required": false,
            "description": "`true` = solo los espacios raíz (cualquier otro valor se ignora).",
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/Activo"
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Referencia"
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      },
      "post": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "crearEspacio",
        "summary": "Dar de alta un espacio",
        "description": "`codigo` obligatorio y único en la organización (si no, `409`). Con `id` (uuid del integrador) es idempotente: repetirlo devuelve el mismo con `repetido: true`. Un espacio raíz puede colgar de un centro propio (`ubicacion_id`) o de un centro de cliente (`cliente_centro_id`), no de los dos. `refs_externas` (función `mnt_referencias`; si no, `403` `FUNCION_APAGADA`): `null` = quitar una; si un valor ya es de otro activo o espacio, `409` con `code` `MNT_REF_EN_USO` y el espacio no se crea. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:write",
        "responses": {
          "201": {
            "description": "Creado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Espacio"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/Repetido"
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          },
          "422": {
            "$ref": "#/components/responses/IdempotenciaOtra"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EspacioEscritura"
              }
            }
          }
        }
      }
    },
    "/v1/espacios/{id}": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "obtenerEspacio",
        "summary": "Un espacio",
        "description": "`404` si no existe o es de otra organización. Con sus `refs_externas`. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "El espacio.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Espacio"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      },
      "patch": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "cambiarEspacio",
        "summary": "Cambiar un espacio",
        "description": "Solo lo que se envía. `padre_id` no se cambia por la API (mover un espacio va por la aplicación). `refs_externas` (función `mnt_referencias`; si no, `403` `FUNCION_APAGADA`): `null` = quitar una; si un valor ya es de otro activo o espacio, `409` con `code` `MNT_REF_EN_USO` y no cambia nada (las referencias van primero). Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:write",
        "responses": {
          "200": {
            "description": "Cambiado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Espacio"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          },
          "422": {
            "$ref": "#/components/responses/IdempotenciaOtra"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EspacioEscritura"
              }
            }
          }
        }
      }
    },
    "/v1/activos": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "listarActivos",
        "summary": "Activos (equipos e instalaciones)",
        "description": "Por `nombre` ascendente e `id`. Cada uno con sus `refs_externas`. Con `ref`, solo los que tienen esa referencia externa EXACTA. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "Activos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Activo"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "espacio_id",
            "in": "query",
            "required": false,
            "description": "Activos ubicados directamente en este espacio.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "categoria_id",
            "in": "query",
            "required": false,
            "description": "De esta categoría.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Estado del activo.",
            "schema": {
              "$ref": "#/components/schemas/EstadoActivo"
            }
          },
          {
            "$ref": "#/components/parameters/Activo"
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Referencia"
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      },
      "post": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "crearActivo",
        "summary": "Dar de alta un activo",
        "description": "Mismas reglas que la aplicación: `nombre` obligatorio (≤ 120), `codigo` único en la organización (`409`), estado distinto de `baja`, referencias de la organización. `coste_adquisicion` solo con `mantenimiento:costes` (sin él, `403`). Con `id` es idempotente (`200`, `repetido: true`). `refs_externas` (función `mnt_referencias`; si no, `403` `FUNCION_APAGADA`): `null` = quitar una; si un valor ya es de otro activo o espacio, `409` con `code` `MNT_REF_EN_USO` y el activo no se crea. Posición propia con `lat` + `lng` (+ `precision_m`), origen `api` (función `mnt_posicion_activos`; si no, `403` `FUNCION_APAGADA`). Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:write",
        "responses": {
          "201": {
            "description": "Creado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Activo"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/Repetido"
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          },
          "422": {
            "$ref": "#/components/responses/IdempotenciaOtra"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActivoEscritura"
              }
            }
          }
        }
      }
    },
    "/v1/activos/{id}": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "obtenerActivo",
        "summary": "Un activo, con sus componentes y documentos",
        "description": "Los componentes son sus hijos directos; de los documentos vigentes, solo los metadatos (el fichero no se expone). Con sus `refs_externas` y su posición propia (`lat`, `lng`, `posicion_*`; vacía = está donde su espacio). Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "El activo.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ActivoDetalle"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      },
      "patch": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "cambiarActivo",
        "summary": "Cambiar un activo",
        "description": "Solo lo que se envía. La baja va por la aplicación. Si el activo tiene garantías detalladas, `garantia_hasta` la calcula Talavora y cambiarla da `409`. `refs_externas` (función `mnt_referencias`; si no, `403` `FUNCION_APAGADA`): `null` = quitar una; si un valor ya es de otro activo o espacio, `409` con `code` `MNT_REF_EN_USO` y no cambia nada (las referencias van primero). Posición propia con `lat` + `lng` (+ `precision_m`), origen `api` (función `mnt_posicion_activos`; si no, `403` `FUNCION_APAGADA`). Las dos `null` = quitarla. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:write",
        "responses": {
          "200": {
            "description": "Cambiado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Activo"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          },
          "422": {
            "$ref": "#/components/responses/IdempotenciaOtra"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActivoEscritura"
              }
            }
          }
        }
      }
    },
    "/v1/incidencias": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "listarIncidencias",
        "summary": "Incidencias (avisos de avería)",
        "description": "Por `created_at` descendente e `id`. No incluye quién la comunicó. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "Incidencias.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Incidencia"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Estado.",
            "schema": {
              "$ref": "#/components/schemas/EstadoIncidencia"
            }
          },
          {
            "name": "prioridad",
            "in": "query",
            "required": false,
            "description": "Prioridad.",
            "schema": {
              "$ref": "#/components/schemas/Prioridad"
            }
          },
          {
            "name": "activo_id",
            "in": "query",
            "required": false,
            "description": "De este activo.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "espacio_id",
            "in": "query",
            "required": false,
            "description": "De este espacio.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      },
      "post": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "crearIncidencia",
        "summary": "Comunicar una incidencia",
        "description": "Nace en estado `nueva`, con `origen = api`, sin persona que la comunica y con código `INC-AAAA-NNNN`, y entra en la bandeja de la organización. Si se indica el activo y no el espacio, toma el espacio del activo. El activo y el espacio tienen que ser de la organización (si no, `409`). Con `id` (uuid del integrador) es idempotente, también si dos peticiones con el mismo `id` llegan a la vez. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:write",
        "responses": {
          "201": {
            "description": "Creada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Incidencia"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/Repetido"
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          },
          "422": {
            "$ref": "#/components/responses/IdempotenciaOtra"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IncidenciaNueva"
              }
            }
          }
        }
      }
    },
    "/v1/incidencias/{id}": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "obtenerIncidencia",
        "summary": "Una incidencia",
        "description": "`404` si no existe o es de otra organización. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "La incidencia.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Incidencia"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/planes": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "listarPlanes",
        "summary": "Planes de mantenimiento preventivo",
        "description": "Por `nombre` e `id`. Si la instalación aún no tiene el módulo de preventivo, `404` con un mensaje explícito. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "Planes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Plan"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/planes/{id}": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "obtenerPlan",
        "summary": "Un plan, con sus asignaciones a activos o espacios",
        "description": "Hasta 1.000 asignaciones. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "El plan.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PlanDetalle"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/contadores": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "listarContadores",
        "summary": "Contadores de los activos (horas, km, ciclos, kWh, m³…)",
        "description": "Por `nombre` e `id`, con la última lectura (`valor_actual`, `leido_at`). En uno `acumulativo` el valor nunca baja salvo un reinicio, que anota una persona con su motivo. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "Contadores.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/Contador"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "activo_id",
            "in": "query",
            "required": false,
            "description": "De este activo.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/Activo"
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/contadores/{id}": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "obtenerContador",
        "summary": "Un contador, con sus 50 últimas lecturas",
        "description": "`404` si no existe o es de otra organización. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "El contador.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ContadorDetalle"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/lecturas": {
      "post": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "crearLectura",
        "summary": "Anotar una lectura de un contador",
        "description": "Una lectura PUNTUAL (no una serie de segundos: un dispositivo agrega en origen, como mucho una por contador y cuarto de hora). Idempotente por `id`: reenviar la misma devuelve `200` con `repetido: true`. La base la valida: en un contador acumulativo no puede ser menor que la anterior ni mayor que una posterior (`409` con `code` `MNT_LECTURA_BAJA` o `MNT_LECTURA_SUPERA`), ni del futuro (5 minutos de margen, `400` con `code` `MNT_LECTURA_FUTURA`). Si llega al umbral de un preventivo por uso, la orden se crea en el acto. Un reinicio de contador no entra por la API. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:write",
        "responses": {
          "201": {
            "description": "Guardada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Lectura"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "200": {
            "$ref": "#/components/responses/Repetido"
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "409": {
            "$ref": "#/components/responses/Conflicto"
          },
          "422": {
            "$ref": "#/components/responses/IdempotenciaOtra"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LecturaNueva"
              }
            }
          }
        }
      }
    },
    "/v1/mantenimiento/ordenes": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "listarOrdenesMantenimiento",
        "summary": "Órdenes de trabajo de mantenimiento",
        "description": "Por `created_at` descendente e `id`. Sin datos personales: ni el técnico (solo `con_tecnico`), ni quién la creó, ni la descripción, ni las notas. `desde`/`hasta` filtran por el día de ALTA; `updated_since` por `updated_at` (las órdenes sin `updated_at` cuentan por su alta o su cierre). Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "Órdenes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "items": {
                        "$ref": "#/components/schemas/OrdenMantenimiento"
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "name": "estado",
            "in": "query",
            "required": false,
            "description": "Estado.",
            "schema": {
              "$ref": "#/components/schemas/EstadoOrden"
            }
          },
          {
            "name": "tipo",
            "in": "query",
            "required": false,
            "description": "Tipo de mantenimiento.",
            "schema": {
              "$ref": "#/components/schemas/TipoMantenimiento"
            }
          },
          {
            "name": "activo_id",
            "in": "query",
            "required": false,
            "description": "De este activo.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "espacio_id",
            "in": "query",
            "required": false,
            "description": "De este espacio.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "required": false,
            "description": "Alta desde (AAAA-MM-DD = 00:00 UTC, o fecha y hora ISO 8601).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "required": false,
            "description": "Alta hasta (AAAA-MM-DD incluye ese día entero, o fecha y hora ISO 8601).",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          }
        ]
      }
    },
    "/v1/mantenimiento/ordenes/{id}": {
      "get": {
        "tags": [
          "Mantenimiento"
        ],
        "operationId": "obtenerOrdenMantenimiento",
        "summary": "Una orden de mantenimiento",
        "description": "Con paradas, materiales y minutos por tipo (trabajo, desplazamiento, espera). Con `mantenimiento:costes`, además `coste` y el coste, el precio y el IVA de cada material. Además del permiso, la organización tiene que tener Mantenimiento activado (si no, `403`).",
        "x-talavora-scope": "mantenimiento:read",
        "responses": {
          "200": {
            "description": "La orden.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/OrdenMantenimientoDetalle"
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/PeticionNoValida"
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/SinPermiso"
          },
          "404": {
            "$ref": "#/components/responses/NoEncontrado"
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ]
      }
    },
    "/v1/bi": {
      "get": {
        "tags": [
          "BI"
        ],
        "operationId": "catalogoBi",
        "summary": "Catálogo de vistas de BI y sus columnas",
        "description": "Las vistas que puede leer esta clave, con su ruta y sus columnas en orden (sin las de importes si la clave no tiene `mantenimiento:costes`). Requiere el permiso `bi:read` DICHO EN LA CLAVE (el comodín `*` de las claves antiguas no lo abre), Mantenimiento activado y la función «Vistas para BI» (`mnt_bi`). Las columnas de importes solo salen con `mantenimiento:costes`. Guía para Power BI, Tableau y Metabase en la documentación.",
        "x-talavora-scope": "bi:read",
        "x-talavora-funcion": "mnt_bi",
        "responses": {
          "200": {
            "description": "Las vistas.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "version"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "vista": {
                            "$ref": "#/components/schemas/VistaBi"
                          },
                          "ruta": {
                            "type": "string",
                            "examples": [
                              "/v1/bi/activos"
                            ]
                          },
                          "columnas": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    },
                    "version": {
                      "type": "integer",
                      "const": 1
                    }
                  }
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "description": "Falta `bi:read` (el comodín `*` no vale), la organización no tiene Mantenimiento, o la función `mnt_bi` está apagada (`code` `FUNCION_APAGADA`, `funcion` `mnt_bi`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "scope": {
                    "value": {
                      "error": "Falta scope bi:read"
                    }
                  },
                  "modulo": {
                    "value": {
                      "error": "Mantenimiento no está activado en esta empresa."
                    }
                  },
                  "funcion": {
                    "value": {
                      "error": "Esta función no está activada en tu empresa.",
                      "code": "FUNCION_APAGADA",
                      "funcion": "mnt_bi"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          },
          "503": {
            "description": "No se ha podido comprobar si la función está activada: reintentar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bi/{vista}": {
      "get": {
        "tags": [
          "BI"
        ],
        "operationId": "leerVistaBi",
        "summary": "Una vista de BI, por páginas (JSON o CSV)",
        "description": "Filas de UNA vista de la organización de la clave (la base filtra por ella; no hay forma de pedir otra), en orden estable por su clave (`id`; `ot_id` en `sla_ot`, `activo_id` en `fiabilidad_activos`), hasta `limit` (1 a 1000, por defecto 1000). `cursor` es opaco y solo vale para la vista que lo dio. Sin datos personales: de las personas solo ids; ni descripciones, ni notas, ni quién comunicó o anotó. JSON: `{ data, next_cursor, columnas }`. CSV (`format=csv` o `Accept: text/csv`): UTF-8 con BOM, coma y CRLF, fechas ISO 8601 y punto decimal; la página siguiente, en las cabeceras `X-Next-Cursor` y `Link: <…>; rel=\"next\"`. Requiere el permiso `bi:read` DICHO EN LA CLAVE (el comodín `*` de las claves antiguas no lo abre), Mantenimiento activado y la función «Vistas para BI» (`mnt_bi`). Las columnas de importes solo salen con `mantenimiento:costes`. Guía para Power BI, Tableau y Metabase en la documentación.",
        "x-talavora-scope": "bi:read",
        "x-talavora-funcion": "mnt_bi",
        "parameters": [
          {
            "$ref": "#/components/parameters/VistaBi"
          },
          {
            "$ref": "#/components/parameters/Limite"
          },
          {
            "$ref": "#/components/parameters/Cursor"
          },
          {
            "$ref": "#/components/parameters/FormatoBi"
          }
        ],
        "responses": {
          "200": {
            "description": "Una página.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "next_cursor",
                    "columnas"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "maxItems": 1000,
                      "description": "Filas con SOLO las columnas visibles, en su orden.",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "next_cursor": {
                      "$ref": "#/components/schemas/SiguienteCursor"
                    },
                    "columnas": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Las columnas de cada fila, en su orden."
                    }
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string"
                },
                "example": "id,codigo,nombre\r\n3f2b8c1e-5d4a-4e7b-9c6d-1a2b3c4d5e6f,BOM-01,Bomba de calor\r\n"
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              },
              "X-Next-Cursor": {
                "description": "Solo en CSV: el cursor de la página siguiente (si la hay).",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "Solo en CSV: `</v1/bi/<vista>?…&cursor=…>; rel=\"next\"` (si hay más).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`limit` fuera de 1–1000, `format` desconocido o `cursor` de otra vista o manipulado (volver a empezar sin cursor).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Vista desconocida (el texto dice cuáles hay), o la instalación aún no tiene las vistas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/NoAutenticado"
          },
          "403": {
            "description": "Falta `bi:read` (el comodín `*` no vale), la organización no tiene Mantenimiento, o la función `mnt_bi` está apagada (`code` `FUNCION_APAGADA`, `funcion` `mnt_bi`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "scope": {
                    "value": {
                      "error": "Falta scope bi:read"
                    }
                  },
                  "modulo": {
                    "value": {
                      "error": "Mantenimiento no está activado en esta empresa."
                    }
                  },
                  "funcion": {
                    "value": {
                      "error": "Esta función no está activada en tu empresa.",
                      "code": "FUNCION_APAGADA",
                      "funcion": "mnt_bi"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/DemasiadasPeticiones"
          },
          "500": {
            "$ref": "#/components/responses/ErrorInterno"
          },
          "503": {
            "description": "No se ha podido comprobar si la función está activada: reintentar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dispositivos/lecturas": {
      "post": {
        "tags": [
          "Dispositivos"
        ],
        "operationId": "recibirLecturasDispositivo",
        "summary": "Un lote de lecturas de un dispositivo",
        "description": "Con la credencial del aparato (`tlv_dev_…`), NO con una clave de API; función «Dispositivos conectados» (`mnt_iot`) y Mantenimiento activados. Hasta 500 lecturas y 256 KB por petición, y su cupo de lecturas por minuto (`429` + `Retry-After`; el cupo de 600 por hora de las claves no aplica). Cada `punto` es un canal mapeado a un contador; Talavora convierte el valor bruto (× factor + desplazamiento) y guarda COMO MUCHO una lectura por punto e intervalo (la última, con cuántas llegaron, la mínima y la máxima); los umbrales miran todas. **Idempotente por (dispositivo, punto, hora de la lectura)**: reenviar el lote no duplica nada (lo ya recibido sale `repetida`; el intervalo que ya se guardó, también). No usa `Idempotency-Key`. Respuesta `200` también con lecturas rechazadas, una a una en `detalle` con su `code`: no reintentar esas. `503` + `Retry-After` si la base no responde: reenviar el lote.",
        "x-talavora-funcion": "mnt_iot",
        "security": [
          {
            "claveDispositivo": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VersionDispositivo"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoteLecturasDispositivo"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Entregado (también con lecturas rechazadas, una a una en `detalle`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RespuestaLecturasDispositivo"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "description": "El cuerpo no es JSON, no es una lista de lecturas (ni `{ lecturas: [...] }`), está vacío o trae más de 500: corregir, no reintentar igual.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "json": {
                    "value": {
                      "error": "El cuerpo no es JSON."
                    }
                  },
                  "muchas": {
                    "value": {
                      "error": "Como mucho 500 lecturas por petición: parte el lote."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/DispositivoNoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/DispositivoApagado"
          },
          "413": {
            "$ref": "#/components/responses/DemasiadoGrande"
          },
          "429": {
            "$ref": "#/components/responses/CupoDispositivo"
          },
          "503": {
            "$ref": "#/components/responses/Pasajero"
          }
        }
      }
    },
    "/v1/dispositivos/latido": {
      "post": {
        "tags": [
          "Dispositivos"
        ],
        "operationId": "latidoDispositivo",
        "summary": "«Sigo vivo», versión y reloj del dispositivo",
        "description": "Con la credencial del aparato (`tlv_dev_…`), NO con una clave de API; función `mnt_iot`. Cuerpo opcional. Devuelve la hora del servidor, el desvío del reloj del aparato (si manda `hora`), su cupo y los puntos activos que debe mandar. Cada 5–15 minutos basta: más a menudo no escribe nada (solo se anota si ha pasado un minuto o cambia la versión o el reloj), así que repetirlo no tiene efectos. No usa `Idempotency-Key`.",
        "x-talavora-funcion": "mnt_iot",
        "security": [
          {
            "claveDispositivo": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VersionDispositivo"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LatidoDispositivo"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recibido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RespuestaLatidoDispositivo"
                }
              }
            },
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/IdPeticion"
              }
            }
          },
          "400": {
            "description": "El cuerpo no es JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/DispositivoNoAutenticado"
          },
          "403": {
            "$ref": "#/components/responses/DispositivoApagado"
          },
          "413": {
            "$ref": "#/components/responses/DemasiadoGrande"
          },
          "429": {
            "$ref": "#/components/responses/FrenoDispositivo"
          },
          "503": {
            "$ref": "#/components/responses/Pasajero"
          }
        }
      }
    }
  },
  "webhooks": {
    "fichaje.creado": {
      "post": {
        "summary": "Se ha registrado un fichaje (no los importados)",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "fichaje.creado"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "fichaje.creado"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosFichajeCreado"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "fichaje.editado": {
      "post": {
        "summary": "Se ha corregido la hora o el tipo de un fichaje",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "fichaje.editado"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "fichaje.editado"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosFichajeEditado"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "fichaje.anulado": {
      "post": {
        "summary": "Se ha anulado un fichaje (un fichaje no se borra: se anula con motivo)",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "fichaje.anulado"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "fichaje.anulado"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosFichajeAnulado"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "ausencia.aprobada": {
      "post": {
        "summary": "Se ha aprobado una ausencia",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "ausencia.aprobada"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "ausencia.aprobada"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosAusenciaAprobada"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "incidencia.creada": {
      "post": {
        "summary": "Se ha creado una incidencia de mantenimiento (cualquier origen)",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "incidencia.creada"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "incidencia.creada"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosIncidenciaCreada"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "ot_mantenimiento.creada": {
      "post": {
        "summary": "Se ha creado una orden de mantenimiento (o una orden pasa a serlo)",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "ot_mantenimiento.creada"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "ot_mantenimiento.creada"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosOtMntCreada"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "ot_mantenimiento.asignada": {
      "post": {
        "summary": "Una orden de mantenimiento pasa a tener técnico, brigada o proveedor (o cambia)",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "ot_mantenimiento.asignada"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "ot_mantenimiento.asignada"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosOtMntAsignada"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "ot_mantenimiento.cerrada": {
      "post": {
        "summary": "Se ha cerrado una orden de mantenimiento",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "ot_mantenimiento.cerrada"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "ot_mantenimiento.cerrada"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosOtMntCerrada"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "activo.estado_cambiado": {
      "post": {
        "summary": "Un activo entra en (o sale de) averiado o fuera de servicio",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "activo.estado_cambiado"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "activo.estado_cambiado"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosActivoEstado"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "preventivo.vencido": {
      "post": {
        "summary": "Un preventivo sigue abierto y su fecha límite ya pasó (barrido diario, 05:30 UTC)",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "preventivo.vencido"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "preventivo.vencido"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosPreventivoVencido"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    },
    "test.ping": {
      "post": {
        "summary": "La prueba del botón «Test» (firmada igual que un evento real)",
        "description": "Talavora hace `POST` a la URL del webhook con este cuerpo, firmado con `X-Talavora-Signature`. Cualquier respuesta `2xx` cuenta como entregada.",
        "parameters": [
          {
            "name": "X-Talavora-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "const": "test.ping"
            }
          },
          {
            "name": "X-Talavora-Version-Evento",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string",
              "examples": [
                "1"
              ]
            }
          },
          {
            "name": "X-Talavora-Signature",
            "in": "header",
            "required": true,
            "description": "`t=<segundos unix>,v1=<hex HMAC-SHA256(secreto, t + \".\" + cuerpo)>`",
            "schema": {
              "type": "string",
              "pattern": "^t=\\d+,v1=[0-9a-f]{64}$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "evento",
                  "datos",
                  "enviado_en"
                ],
                "properties": {
                  "evento": {
                    "type": "string",
                    "const": "test.ping"
                  },
                  "enviado_en": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "datos": {
                    "$ref": "#/components/schemas/DatosPrueba"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Entregado. Cualquier otro código (o no contestar en 10 s) cuenta como fallo y se reintenta."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "claveApi": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tlv_pub_ + 40 letras y números",
        "description": "Clave de API de la organización. La crea un administrador en Configuración → API e integraciones, se enseña una sola vez, caduca (1 año por defecto, 2 como máximo) y se puede revocar. Cada clave es de UNA organización: el servidor filtra siempre por ella."
      },
      "claveDispositivo": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tlv_dev_ + 43 caracteres base64url",
        "description": "Credencial de UN dispositivo (Mantenimiento → Configuración → Dispositivos): se enseña una sola vez, se rota y se revoca allí. Va en `Authorization: Bearer tlv_dev_…` o, si la pasarela no deja tocar esa cabecera, en `X-Talavora-Dispositivo: tlv_dev_…`. Solo sirve para `/v1/dispositivos/…`; una clave de API no entra por ahí ni la del aparato en el resto de la API. Nunca en la URL.",
        "x-talavora-patron": "^tlv_dev_[A-Za-z0-9_-]{43}$"
      }
    },
    "headers": {
      "IdPeticion": {
        "description": "Identificador de la petición: el que mande en `x-request-id` (8 a 128 caracteres `A-Za-z0-9._:-`) o uno nuevo. Cítelo al pedir ayuda.",
        "schema": {
          "type": "string"
        }
      },
      "RespuestaRepetida": {
        "description": "`true` cuando la respuesta es la guardada de una petición anterior con la misma `Idempotency-Key`.",
        "schema": {
          "type": "string",
          "const": "true"
        }
      }
    },
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Limite": {
        "name": "limit",
        "in": "query",
        "required": false,
        "description": "Filas por página (1 a 1000; por defecto 1000). Fuera de rango, `400`.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 1000,
          "default": 1000
        }
      },
      "Cursor": {
        "name": "cursor",
        "in": "query",
        "required": false,
        "description": "El `next_cursor` de la respuesta anterior, para pedir la página siguiente CON LOS MISMOS FILTROS. Opaco; solo vale para el listado que lo dio (si no, `400`).",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]+$",
          "maxLength": 2048
        }
      },
      "Activo": {
        "name": "activo",
        "in": "query",
        "required": false,
        "description": "`true` = solo los de alta; `false` = solo los de baja.",
        "schema": {
          "type": "boolean"
        }
      },
      "UpdatedSince": {
        "name": "updated_since",
        "in": "query",
        "required": false,
        "description": "Modificados desde (AAAA-MM-DD = 00:00 UTC, o fecha y hora ISO 8601).",
        "schema": {
          "type": "string"
        }
      },
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Repetir la petición con la misma clave (24 h, por clave de API) devuelve la misma respuesta sin hacer nada dos veces (`Idempotent-Replayed: true`). Con otra petición, `422`; si la primera sigue en curso, `409`.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9._:-]{1,200}$"
        }
      },
      "Referencia": {
        "name": "ref",
        "in": "query",
        "required": false,
        "description": "Por referencia externa EXACTA: `sistema:valor` (`rfid:E2801160600002084DBE9E71`, `ifc:2O2Fr$t4X7Zf8NOew3FLOH`, `inventario:INV-2024/7`, `erp:…`, `otro:…`; `catastro:…` solo en espacios) o solo el valor (se busca en todos los sistemas en que tiene sentido). Se normaliza como al guardar (EPC en mayúsculas sin espacios; un GUID de 36 pasa a GlobalId de IFC). Sistema conocido sin valor o más de 200 caracteres: `400`.",
        "schema": {
          "type": "string",
          "maxLength": 200
        }
      },
      "VistaBi": {
        "name": "vista",
        "in": "path",
        "required": true,
        "schema": {
          "$ref": "#/components/schemas/VistaBi"
        }
      },
      "FormatoBi": {
        "name": "format",
        "in": "query",
        "required": false,
        "description": "`csv` para CSV (también con `Accept: text/csv`); `json` por defecto. Otro valor: `400`.",
        "schema": {
          "type": "string",
          "enum": [
            "json",
            "csv"
          ],
          "default": "json"
        }
      },
      "VersionDispositivo": {
        "name": "X-Talavora-Version",
        "in": "header",
        "required": false,
        "description": "Versión del firmware o del flujo de la pasarela (hasta 60 caracteres; se ve en la ficha del aparato). También vale `version` en el cuerpo.",
        "schema": {
          "type": "string",
          "maxLength": 60
        }
      }
    },
    "responses": {
      "PeticionNoValida": {
        "description": "Parámetro, cursor o cuerpo no válido.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NoAutenticado": {
        "description": "Falta la clave, no es válida, está revocada o ha caducado.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "sinClave": {
                "value": {
                  "error": "Clave Talavora (tlv_pub_...) requerida en Authorization: Bearer"
                }
              },
              "invalida": {
                "value": {
                  "error": "API key inválida"
                }
              },
              "desactivada": {
                "value": {
                  "error": "API key desactivada"
                }
              },
              "caducada": {
                "value": {
                  "error": "API key expirada"
                }
              }
            }
          }
        }
      },
      "SinPermiso": {
        "description": "A la clave le falta el permiso (scope), o la organización no tiene activado el módulo o la función que pide la ruta.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "scope": {
                "value": {
                  "error": "Falta scope fichajes:read"
                }
              },
              "modulo": {
                "value": {
                  "error": "Mantenimiento no está activado en esta empresa."
                }
              },
              "funcion": {
                "value": {
                  "error": "Esta función no está activada en tu empresa.",
                  "code": "FUNCION_APAGADA",
                  "funcion": "mnt_api"
                }
              }
            }
          }
        }
      },
      "NoEncontrado": {
        "description": "La ruta no existe, o el recurso no existe o es de otra organización.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MetodoNoAdmitido": {
        "description": "Método no admitido en esa ruta.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflicto": {
        "description": "Código repetido, referencia de otra organización, `id` ya usado para otra cosa, regla de la base (con su `code`) o petición idempotente aún en curso.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "IdempotenciaOtra": {
        "description": "Esa `Idempotency-Key` ya se usó con otra petición (otro método, ruta o cuerpo).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "DemasiadasPeticiones": {
        "description": "Más de 600 peticiones en una hora con esta clave. El texto dice cuántos segundos esperar.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Límite de 600 peticiones por hora para esta clave. Reintenta en 120 s."
            }
          }
        }
      },
      "ErrorInterno": {
        "description": "Error inesperado: queda registrado con el identificador de la petición (`x-request-id`).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Repetido": {
        "description": "Ya existía con ese `id` (reenvío): se devuelve el mismo, sin crear otro.",
        "headers": {
          "x-request-id": {
            "$ref": "#/components/headers/IdPeticion"
          },
          "Idempotent-Replayed": {
            "$ref": "#/components/headers/RespuestaRepetida"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "data": {
                  "type": "object"
                },
                "repetido": {
                  "type": "boolean",
                  "const": true
                }
              },
              "required": [
                "data"
              ]
            }
          }
        }
      },
      "DispositivoNoAutenticado": {
        "description": "Sin credencial de dispositivo, no válida o revocada (con `WWW-Authenticate: Bearer realm=\"Talavora\"`). Parar y avisar: reintentar no lo arregla.",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "sinCredencial": {
                "value": {
                  "error": "credencial de dispositivo requerida (Authorization: Bearer tlv_dev_…)"
                }
              },
              "invalida": {
                "value": {
                  "error": "credencial de dispositivo no válida"
                }
              },
              "revocada": {
                "value": {
                  "error": "credencial de dispositivo revocada"
                }
              }
            }
          }
        }
      },
      "DispositivoApagado": {
        "description": "Mantenimiento o la función «Dispositivos conectados» (`mnt_iot`) no están activados en la organización del aparato.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Mantenimiento o los dispositivos conectados no están activados en esta empresa."
            }
          }
        }
      },
      "CupoDispositivo": {
        "description": "El aparato se ha pasado de su cupo de lecturas por minuto (600 por defecto; de 10 a 6.000, ventana fija de 60 s; un latido no cuenta), o de los frenos de ráfaga que se aplican antes de la base (peticiones por IP y por aparato, credenciales no válidas desde la misma IP). No ha entrado NADA del lote: reenviarlo entero tras `Retry-After`.",
        "headers": {
          "Retry-After": {
            "description": "Segundos que esperar antes de reintentar.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error",
                "espera_seg"
              ],
              "properties": {
                "error": {
                  "type": "string"
                },
                "espera_seg": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "example": {
              "error": "Cupo de 600 lecturas por minuto superado: no ha entrado nada del lote. Reintenta en 42 s.",
              "espera_seg": 42
            }
          }
        }
      },
      "Pasajero": {
        "description": "La plataforma no responde ahora (o tarda más de 10 s). Reintentar tras `Retry-After`: el reintento no duplica nada.",
        "headers": {
          "Retry-After": {
            "description": "Segundos que esperar antes de reintentar.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "no disponible, reintentar"
            }
          }
        }
      },
      "DemasiadoGrande": {
        "description": "Más de 262144 bytes (256 KB) de cuerpo: partir el lote (500 lecturas caben con holgura).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "FrenoDispositivo": {
        "description": "Demasiadas peticiones antes de llegar a la base: por IP (240 de golpe, 4 por segundo), por aparato (6 latidos por minuto; 60 peticiones de lecturas por minuto) o demasiadas credenciales no válidas desde la misma IP. Esperar `Retry-After` y reintentar.",
        "headers": {
          "Retry-After": {
            "description": "Segundos que esperar antes de reintentar.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error",
                "espera_seg"
              ],
              "properties": {
                "error": {
                  "type": "string"
                },
                "espera_seg": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "example": {
              "error": "Demasiadas peticiones (latidos de este dispositivo). Reintenta en 10 s.",
              "espera_seg": 10
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Qué ha pasado, en castellano y pensado para una persona."
          },
          "code": {
            "type": "string",
            "description": "Código estable para programar contra él, cuando lo hay (p. ej. `MNT_LECTURA_BAJA`)."
          },
          "funcion": {
            "type": "string",
            "description": "Con `code` `FUNCION_APAGADA`: la función de la organización que hace falta y está apagada (p. ej. `mnt_api`)."
          }
        },
        "required": [
          "error"
        ]
      },
      "SiguienteCursor": {
        "type": [
          "string",
          "null"
        ],
        "description": "Cursor de la página siguiente, o `null` si no hay más. Con una página llena de 1.000 puede venir informado y la siguiente llegar vacía."
      },
      "Scope": {
        "type": "string",
        "enum": [
          "empleados:read",
          "fichajes:read",
          "ausencias:read",
          "clientes:read",
          "ordenes:read",
          "facturas:read",
          "mantenimiento:read",
          "mantenimiento:write",
          "mantenimiento:costes",
          "bi:read"
        ]
      },
      "Salud": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "base_ms": {
            "type": "integer",
            "description": "Milisegundos de la consulta de comprobación."
          },
          "motivo": {
            "type": "string"
          },
          "cacheada": {
            "type": "boolean"
          }
        }
      },
      "Empleado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "nombre": {
            "type": [
              "string",
              "null"
            ]
          },
          "apellidos": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "rol": {
            "type": [
              "string",
              "null"
            ],
            "description": "EMPLEADO, SUBADMIN, ADMIN…"
          },
          "activo": {
            "type": [
              "boolean",
              "null"
            ]
          }
        }
      },
      "EmpleadoDetalle": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Empleado"
          },
          {
            "type": "object",
            "properties": {
              "fecha_alta": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date"
              },
              "fecha_nacimiento": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date"
              }
            }
          }
        ]
      },
      "Fichaje": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "empleado_id": {
            "type": "string",
            "format": "uuid"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "ENTRADA",
              "SALIDA",
              "PAUSA_INICIO",
              "PAUSA_FIN"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "metodo": {
            "type": [
              "string",
              "null"
            ],
            "description": "Vía de registro.",
            "examples": [
              "MANUAL",
              "GEOFENCE",
              "QR",
              "AUTO",
              "CODIGO",
              "KIOSKO",
              "NFC",
              "KIOSKO_DIFERIDO",
              "IMPORTACION"
            ]
          },
          "validado": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "lat": {
            "type": [
              "number",
              "null"
            ]
          },
          "lng": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "EstadoAusencia": {
        "type": "string",
        "description": "Coexisten dos grafías de los estados finales; trátelas como iguales: APROBADO/APROBADA, RECHAZADO/RECHAZADA, CANCELADA/CANCELADO.",
        "examples": [
          "PENDIENTE",
          "PENDIENTE_JUSTIFICANTE",
          "APROBADO",
          "APROBADA",
          "RECHAZADO",
          "RECHAZADA",
          "CANCELADA",
          "CANCELADO"
        ]
      },
      "Ausencia": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "empleado_id": {
            "type": "string",
            "format": "uuid"
          },
          "tipo": {
            "type": [
              "string",
              "null"
            ]
          },
          "fecha_inicio": {
            "type": "string",
            "format": "date"
          },
          "fecha_fin": {
            "type": "string",
            "format": "date"
          },
          "dias": {
            "type": [
              "number",
              "null"
            ]
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoAusencia"
          },
          "motivo": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Cliente": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "nombre": {
            "type": [
              "string",
              "null"
            ]
          },
          "nif": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "telefono": {
            "type": [
              "string",
              "null"
            ]
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ]
          },
          "ciudad": {
            "type": [
              "string",
              "null"
            ]
          },
          "cp": {
            "type": [
              "string",
              "null"
            ]
          },
          "provincia": {
            "type": [
              "string",
              "null"
            ]
          },
          "activo": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Orden": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "titulo": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "type": [
              "string",
              "null"
            ]
          },
          "prioridad": {
            "type": [
              "string",
              "null"
            ]
          },
          "cliente_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "asignado_a": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "fecha_prevista": {
            "type": [
              "string",
              "null"
            ]
          },
          "duracion_estimada_min": {
            "type": [
              "integer",
              "null"
            ]
          },
          "cerrada_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "albaran_numero": {
            "type": [
              "string",
              "null"
            ]
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ]
          },
          "poblacion": {
            "type": [
              "string",
              "null"
            ]
          },
          "cp": {
            "type": [
              "string",
              "null"
            ]
          },
          "provincia": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ParteOrden": {
        "type": "object",
        "properties": {
          "orden": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "codigo": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "titulo": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "estado": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "fecha_prevista": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "cerrada_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "albaran_numero": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "tiempos": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "empleado_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "uuid"
                },
                "tipo": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "inicio": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "fin": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "minutos": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "notas": {
                  "type": [
                    "string",
                    "null"
                  ]
                }
              }
            }
          },
          "materiales": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "concepto": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "cantidad": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "precio": {
                  "type": [
                    "number",
                    "null"
                  ]
                },
                "iva_pct": {
                  "type": [
                    "number",
                    "null"
                  ]
                }
              }
            }
          },
          "firma": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "firmante": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "dni": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "hash_parte": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "firmado_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        }
      },
      "Factura": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "numero_completo": {
            "type": [
              "string",
              "null"
            ]
          },
          "serie": {
            "type": [
              "string",
              "null"
            ]
          },
          "numero": {
            "type": [
              "integer",
              "null"
            ]
          },
          "fecha": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "tipo": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "type": [
              "string",
              "null"
            ]
          },
          "cliente_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "cliente_nombre": {
            "type": [
              "string",
              "null"
            ]
          },
          "cliente_nif": {
            "type": [
              "string",
              "null"
            ]
          },
          "base": {
            "type": [
              "number",
              "null"
            ]
          },
          "iva_total": {
            "type": [
              "number",
              "null"
            ]
          },
          "irpf_pct": {
            "type": [
              "number",
              "null"
            ]
          },
          "irpf_importe": {
            "type": [
              "number",
              "null"
            ]
          },
          "total": {
            "type": [
              "number",
              "null"
            ]
          },
          "fecha_vencimiento": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "verifactu_estado": {
            "type": [
              "string",
              "null"
            ]
          },
          "proyecto_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "FacturaDetalle": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Factura"
          },
          {
            "type": "object",
            "properties": {
              "lineas": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "orden": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "concepto": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "cantidad": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "precio": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "descuento_pct": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "iva_pct": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "base_linea": {
                      "type": [
                        "number",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "EstadoActivo": {
        "type": "string",
        "enum": [
          "en_servicio",
          "averiado",
          "fuera_servicio",
          "en_reparacion",
          "baja"
        ]
      },
      "EstadoIncidencia": {
        "type": "string",
        "enum": [
          "nueva",
          "en_revision",
          "aceptada",
          "convertida",
          "rechazada",
          "duplicada",
          "cerrada"
        ]
      },
      "OrigenIncidencia": {
        "type": "string",
        "enum": [
          "interno",
          "tecnico",
          "portal_cliente",
          "api",
          "publico",
          "proveedor"
        ],
        "description": "Por dónde entró: `interno` (una persona de la organización), `tecnico` (un técnico en campo), `api`, `publico` (el QR público, sin cuenta), `proveedor` (el portal del proveedor). `portal_cliente` está reservado."
      },
      "Prioridad": {
        "type": "string",
        "enum": [
          "baja",
          "normal",
          "alta",
          "urgente"
        ]
      },
      "EstadoOrden": {
        "type": "string",
        "enum": [
          "borrador",
          "planificada",
          "en_curso",
          "pausada",
          "cerrada",
          "facturada",
          "cancelada"
        ]
      },
      "TipoMantenimiento": {
        "type": "string",
        "enum": [
          "correctivo",
          "preventivo",
          "inspeccion",
          "mejora",
          "otro"
        ]
      },
      "Espacio": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "padre_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "tipo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "codigo": {
            "type": "string"
          },
          "nombre": {
            "type": "string"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ]
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ]
          },
          "lat": {
            "type": [
              "number",
              "null"
            ]
          },
          "lng": {
            "type": [
              "number",
              "null"
            ]
          },
          "responsable_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "ubicacion_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "cliente_centro_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "profundidad": {
            "type": "integer"
          },
          "ruta": {
            "type": "string",
            "description": "Ids desde la raíz, separados por `/`."
          },
          "activo": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "refs_externas": {
            "$ref": "#/components/schemas/RefsExternas"
          }
        }
      },
      "EspacioEscritura": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Solo en el alta; la hace idempotente."
          },
          "nombre": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "Obligatorio en el alta."
          },
          "codigo": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,39}$",
            "description": "Obligatorio en el alta."
          },
          "padre_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Solo en el alta."
          },
          "tipo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 4000
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 400
          },
          "lat": {
            "type": [
              "number",
              "null"
            ],
            "minimum": -90,
            "maximum": 90
          },
          "lng": {
            "type": [
              "number",
              "null"
            ],
            "minimum": -180,
            "maximum": 180
          },
          "responsable_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "ubicacion_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "cliente_centro_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "refs_externas": {
            "$ref": "#/components/schemas/RefsExternasEscritura"
          }
        },
        "additionalProperties": false
      },
      "Activo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "nombre": {
            "type": "string"
          },
          "marca": {
            "type": [
              "string",
              "null"
            ]
          },
          "modelo": {
            "type": [
              "string",
              "null"
            ]
          },
          "num_serie": {
            "type": [
              "string",
              "null"
            ]
          },
          "padre_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Activo del que es componente."
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "categoria_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "criticidad": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "baja",
              "media",
              "alta",
              "critica",
              null
            ]
          },
          "estado_activo": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "en_servicio",
              "averiado",
              "fuera_servicio",
              "en_reparacion",
              "baja",
              null
            ]
          },
          "responsable_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "cliente_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "centro_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "fecha_compra": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "coste_adquisicion": {
            "type": [
              "number",
              "null"
            ]
          },
          "instalado_el": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "garantia_hasta": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "ruta": {
            "type": [
              "string",
              "null"
            ]
          },
          "activo": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lat": {
            "type": [
              "number",
              "null"
            ],
            "description": "Latitud PROPIA del activo (WGS84). Sin ella, el activo está donde su espacio."
          },
          "lng": {
            "type": [
              "number",
              "null"
            ],
            "description": "Longitud propia (WGS84), siempre junto con `lat`."
          },
          "posicion_precision_m": {
            "type": [
              "number",
              "null"
            ],
            "description": "Precisión de la posición en metros (la del GPS, si la hay)."
          },
          "posicion_origen": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "manual",
              "gps",
              "importacion",
              "api",
              null
            ],
            "description": "De dónde salió la posición: a mano, GPS del móvil, importación o API."
          },
          "posicion_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo se puso la posición."
          },
          "refs_externas": {
            "$ref": "#/components/schemas/RefsExternas"
          }
        }
      },
      "ActivoDetalle": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Activo"
          },
          {
            "type": "object",
            "properties": {
              "componentes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "codigo": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "nombre": {
                      "type": "string"
                    },
                    "estado_activo": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              },
              "documentos": {
                "type": "array",
                "description": "Metadatos de los documentos vigentes (hasta 200).",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "tipo": {
                      "type": "string"
                    },
                    "titulo": {
                      "type": "string"
                    },
                    "mime": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "bytes": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "sha256": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "emitido_el": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date"
                    },
                    "vence_el": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "ActivoEscritura": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Solo en el alta; la hace idempotente."
          },
          "nombre": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "Obligatorio en el alta."
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._/-]{0,39}$"
          },
          "marca": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "modelo": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "num_serie": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 4000
          },
          "padre_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "categoria_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "responsable_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "criticidad": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "baja",
              "media",
              "alta",
              "critica",
              null
            ]
          },
          "estado_activo": {
            "type": "string",
            "enum": [
              "en_servicio",
              "averiado",
              "fuera_servicio",
              "en_reparacion"
            ]
          },
          "instalado_el": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "garantia_hasta": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_compra": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "coste_adquisicion": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Requiere `mantenimiento:costes`."
          },
          "refs_externas": {
            "$ref": "#/components/schemas/RefsExternasEscritura"
          },
          "lat": {
            "type": [
              "number",
              "null"
            ],
            "minimum": -90,
            "maximum": 90,
            "description": "Posición propia, con `lng` (las dos o ninguna; las dos `null` = quitarla y vuelve a estar donde su espacio). `0, 0` no se admite. Origen `api`. Necesita la función «Posición propia de los activos» (`mnt_posicion_activos`): si no, `403` con `code` `FUNCION_APAGADA`."
          },
          "lng": {
            "type": [
              "number",
              "null"
            ],
            "minimum": -180,
            "maximum": 180
          },
          "precision_m": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100000,
            "description": "Precisión de la posición en metros (se guarda como `posicion_precision_m`)."
          }
        },
        "additionalProperties": false
      },
      "Incidencia": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": "string",
            "pattern": "^INC-[0-9]{4}-[0-9]{4,}$"
          },
          "titulo": {
            "type": "string"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ]
          },
          "prioridad": {
            "$ref": "#/components/schemas/Prioridad"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "lat": {
            "type": [
              "number",
              "null"
            ]
          },
          "lng": {
            "type": [
              "number",
              "null"
            ]
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoIncidencia"
          },
          "motivo_rechazo": {
            "type": [
              "string",
              "null"
            ]
          },
          "origen": {
            "$ref": "#/components/schemas/OrigenIncidencia"
          },
          "ot_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "duplicada_de": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "resuelta_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "IncidenciaNueva": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Opcional; hace la petición idempotente."
          },
          "titulo": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "descripcion": {
            "type": "string",
            "maxLength": 4000
          },
          "prioridad": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Prioridad"
              }
            ],
            "default": "normal"
          },
          "activo_id": {
            "type": "string",
            "format": "uuid"
          },
          "espacio_id": {
            "type": "string",
            "format": "uuid"
          },
          "lat": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "description": "Junto con `lng`."
          },
          "lng": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          }
        },
        "required": [
          "titulo"
        ]
      },
      "Plan": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "nombre": {
            "type": "string"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ]
          },
          "reglamentario": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "normativa": {
            "type": [
              "string",
              "null"
            ]
          },
          "disparo": {
            "type": "string",
            "enum": [
              "calendario",
              "contador",
              "mixto"
            ]
          },
          "frecuencia_valor": {
            "type": [
              "integer",
              "null"
            ]
          },
          "frecuencia_unidad": {
            "type": [
              "string",
              "null"
            ],
            "description": "null en un plan solo por uso."
          },
          "base_calculo": {
            "type": [
              "string",
              "null"
            ]
          },
          "fecha_inicio": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_fin": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "tolerancia_dias": {
            "type": [
              "integer",
              "null"
            ]
          },
          "dias_antelacion": {
            "type": [
              "integer",
              "null"
            ]
          },
          "duracion_estimada_min": {
            "type": [
              "integer",
              "null"
            ]
          },
          "prioridad": {
            "type": [
              "string",
              "null"
            ]
          },
          "responsable_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "brigada_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "estado": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "contador_tipo": {
            "type": [
              "string",
              "null"
            ]
          },
          "contador_unidad": {
            "type": [
              "string",
              "null"
            ]
          },
          "uso_intervalo": {
            "type": [
              "number",
              "null"
            ]
          },
          "uso_antelacion": {
            "type": [
              "number",
              "null"
            ]
          },
          "uso_antelacion_pct": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "PlanDetalle": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Plan"
          },
          {
            "type": "object",
            "properties": {
              "asignaciones": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "plan_id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "activo_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "espacio_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "proxima_fecha": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date"
                    },
                    "ultima_fecha_prevista": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date"
                    },
                    "ultimo_cierre_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "ultima_ot_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "activo": {
                      "type": "boolean"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "uso_contador_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "uso_base": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Lectura de la última ejecución."
                    },
                    "uso_umbral": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "La lectura a la que toca la siguiente; null con una orden abierta."
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "Contador": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "activo_id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "nombre": {
            "type": "string"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "horas",
              "km",
              "ciclos",
              "kwh",
              "m3",
              "unidades",
              "otro"
            ]
          },
          "unidad": {
            "type": "string"
          },
          "acumulativo": {
            "type": "boolean"
          },
          "valor_actual": {
            "type": [
              "number",
              "null"
            ]
          },
          "leido_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "activo": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ContadorDetalle": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Contador"
          },
          {
            "type": "object",
            "properties": {
              "ultimas_lecturas": {
                "type": "array",
                "maxItems": 50,
                "items": {
                  "$ref": "#/components/schemas/Lectura"
                }
              }
            }
          }
        ]
      },
      "Lectura": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "contador_id": {
            "type": "string",
            "format": "uuid"
          },
          "valor": {
            "type": "number"
          },
          "leido_at": {
            "type": "string",
            "format": "date-time"
          },
          "origen": {
            "type": "string",
            "enum": [
              "manual",
              "qr",
              "api",
              "dispositivo",
              "ot",
              "importacion"
            ]
          },
          "ot_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "nota": {
            "type": [
              "string",
              "null"
            ]
          },
          "reinicio": {
            "type": "boolean"
          },
          "motivo": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "LecturaNueva": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Lo pone el integrador: la hace idempotente."
          },
          "contador_id": {
            "type": "string",
            "format": "uuid"
          },
          "valor": {
            "type": [
              "number",
              "string"
            ],
            "description": "Hasta 3 decimales; |valor| ≤ 1e12. También como texto con punto decimal."
          },
          "leido_at": {
            "type": "string",
            "format": "date-time",
            "description": "Por defecto, ahora. Con zona horaria."
          },
          "origen": {
            "type": "string",
            "enum": [
              "api",
              "dispositivo"
            ],
            "default": "api"
          },
          "nota": {
            "type": "string",
            "maxLength": 500
          }
        },
        "required": [
          "contador_id",
          "valor"
        ]
      },
      "OrdenMantenimiento": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "titulo": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoOrden"
          },
          "prioridad": {
            "type": [
              "string",
              "null"
            ]
          },
          "tipo_mantenimiento": {
            "$ref": "#/components/schemas/TipoMantenimiento"
          },
          "activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "incidencia_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "plan_activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "brigada_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "con_tecnico": {
            "type": "boolean",
            "description": "Tiene técnico asignado (quién, no)."
          },
          "fecha_prevista": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_programada": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_limite": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "iniciada_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cerrada_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "cancelada_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "duracion_estimada_min": {
            "type": [
              "integer",
              "null"
            ]
          },
          "modo_fallo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "causa_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "accion_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "garantia_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "reclamacion_garantia": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "OrdenMantenimientoDetalle": {
        "allOf": [
          {
            "$ref": "#/components/schemas/OrdenMantenimiento"
          },
          {
            "type": "object",
            "properties": {
              "paradas": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "causa": {
                      "type": "string"
                    },
                    "causa_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "inicio": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "fin": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "minutos": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "activo_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "afecta_disponibilidad": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "materiales": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "producto_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "concepto": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "cantidad": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "clase": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "proveedor_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "almacen_id": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "uuid"
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "coste": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Solo con `mantenimiento:costes`."
                    },
                    "precio": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Solo con `mantenimiento:costes`."
                    },
                    "iva_pct": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Solo con `mantenimiento:costes`."
                    }
                  }
                }
              },
              "minutos": {
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                },
                "description": "Minutos por tipo (trabajo, desplazamiento, espera)."
              },
              "coste": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Solo con `mantenimiento:costes`.",
                "properties": {
                  "mano": {
                    "type": "number"
                  },
                  "material": {
                    "type": "number"
                  },
                  "externo": {
                    "type": "number"
                  },
                  "total": {
                    "type": "number"
                  },
                  "incompleto": {
                    "type": "boolean",
                    "description": "Hay horas o materiales sin coste."
                  }
                }
              }
            }
          }
        ]
      },
      "DatosPrueba": {
        "type": "object",
        "properties": {
          "mensaje": {
            "type": "string",
            "const": "Webhook de prueba desde Talavora"
          }
        }
      },
      "DatosFichajeCreado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "empleado_id": {
            "type": "string",
            "format": "uuid"
          },
          "tipo": {
            "type": "string"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "metodo": {
            "type": [
              "string",
              "null"
            ]
          },
          "validado": {
            "type": [
              "boolean",
              "null"
            ]
          }
        }
      },
      "DatosFichajeEditado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "empleado_id": {
            "type": "string",
            "format": "uuid"
          },
          "antes": {
            "type": "object",
            "properties": {
              "timestamp": {
                "type": "string",
                "format": "date-time"
              },
              "tipo": {
                "type": "string"
              }
            }
          },
          "despues": {
            "type": "object",
            "properties": {
              "timestamp": {
                "type": "string",
                "format": "date-time"
              },
              "tipo": {
                "type": "string"
              }
            }
          },
          "motivo": {
            "type": [
              "string",
              "null"
            ]
          },
          "editado_por": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          }
        }
      },
      "DatosFichajeAnulado": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "empleado_id": {
            "type": "string",
            "format": "uuid"
          },
          "anulado_at": {
            "type": "string",
            "format": "date-time"
          },
          "motivo": {
            "type": [
              "string",
              "null"
            ]
          },
          "fichaje": {
            "type": "object",
            "description": "Copia del marcaje archivado."
          }
        }
      },
      "DatosAusenciaAprobada": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "empleado_id": {
            "type": "string",
            "format": "uuid"
          },
          "fecha_inicio": {
            "type": "string",
            "format": "date"
          },
          "fecha_fin": {
            "type": "string",
            "format": "date"
          },
          "tipo": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DatosIncidenciaCreada": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": "string"
          },
          "titulo": {
            "type": "string"
          },
          "prioridad": {
            "$ref": "#/components/schemas/Prioridad"
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoIncidencia"
          },
          "origen": {
            "$ref": "#/components/schemas/OrigenIncidencia"
          },
          "activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "description": "Sin datos personales: ni quién la comunicó ni la descripción."
      },
      "DatosOtMntCerrada": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "titulo": {
            "type": [
              "string",
              "null"
            ]
          },
          "tipo_mantenimiento": {
            "$ref": "#/components/schemas/TipoMantenimiento"
          },
          "activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "incidencia_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "cerrada_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "DatosOtMntCreada": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "titulo": {
            "type": [
              "string",
              "null"
            ]
          },
          "tipo_mantenimiento": {
            "$ref": "#/components/schemas/TipoMantenimiento"
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoOrden"
          },
          "prioridad": {
            "type": [
              "string",
              "null"
            ]
          },
          "activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "incidencia_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "plan_activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "fecha_prevista": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_programada": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_limite": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DatosOtMntAsignada": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "titulo": {
            "type": [
              "string",
              "null"
            ]
          },
          "tipo_mantenimiento": {
            "$ref": "#/components/schemas/TipoMantenimiento"
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoOrden"
          },
          "a": {
            "type": "string",
            "enum": [
              "tecnico",
              "brigada",
              "proveedor"
            ],
            "description": "Qué ha cambiado."
          },
          "con_tecnico": {
            "type": "boolean"
          },
          "brigada_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "fecha_prevista": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_programada": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "asignada_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DatosActivoEstado": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "nombre": {
            "type": "string"
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoActivo"
          },
          "estado_anterior": {
            "type": [
              "string",
              "null"
            ]
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "categoria_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "criticidad": {
            "type": [
              "string",
              "null"
            ]
          },
          "cambiado_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "DatosPreventivoVencido": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "codigo": {
            "type": [
              "string",
              "null"
            ]
          },
          "titulo": {
            "type": [
              "string",
              "null"
            ]
          },
          "estado": {
            "$ref": "#/components/schemas/EstadoOrden"
          },
          "activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "espacio_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "plan_activo_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "plan_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "fecha_programada": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "fecha_limite": {
            "type": "string",
            "format": "date"
          },
          "dias_vencido": {
            "type": "integer"
          }
        }
      },
      "RefsExternas": {
        "type": "object",
        "description": "Identificadores del activo o del espacio en otros sistemas, uno por sistema y únicos en la organización por sistema. `ifc`: GlobalId de IFC/BIM (22 caracteres `0-9A-Za-z_$`, el primero de 0 a 3); `rfid`: EPC de la etiqueta (24 o 32 hexadecimales, en mayúsculas); `catastro` (solo espacios): 14 o 20 caracteres, con las letras de control comprobadas; `inventario`, `erp` y `otro`: texto de una línea de 1 a 80 caracteres. Vacío (`{}`) si no tiene ninguna.",
        "additionalProperties": false,
        "properties": {
          "ifc": {
            "type": "string",
            "pattern": "^[0-3][0-9A-Za-z_$]{21}$"
          },
          "rfid": {
            "type": "string",
            "pattern": "^([0-9A-F]{24}|[0-9A-F]{32})$"
          },
          "inventario": {
            "type": "string",
            "maxLength": 80
          },
          "erp": {
            "type": "string",
            "maxLength": 80
          },
          "catastro": {
            "type": "string",
            "pattern": "^[0-9A-Z]{14}([0-9]{4}[A-Z]{2})?$",
            "description": "Solo espacios."
          },
          "otro": {
            "type": "string",
            "maxLength": 80
          }
        }
      },
      "RefsExternasEscritura": {
        "type": "object",
        "description": "`{ sistema: valor | null }`: solo los sistemas que vienen; `null` o `\"\"` = quitarla. Se normaliza antes de guardar (un GUID de 36 pasa a GlobalId de IFC; la EPC, a mayúsculas sin espacios). Formato mal: `400`. Necesita la función «Referencias externas» (`mnt_referencias`): si no, `403` con `code` `FUNCION_APAGADA`. Un valor que ya tiene otro activo o espacio: `409` con `code` `MNT_REF_EN_USO` y no cambia nada (en un alta, no se crea).",
        "additionalProperties": false,
        "properties": {
          "ifc": {
            "type": [
              "string",
              "null"
            ],
            "description": "GlobalId de 22 o GUID de 36."
          },
          "rfid": {
            "type": [
              "string",
              "null"
            ],
            "description": "EPC de 24 o 32 hexadecimales (admite espacios, guiones o `urn:epc:raw:…`)."
          },
          "inventario": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "erp": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "catastro": {
            "type": [
              "string",
              "null"
            ],
            "description": "Solo espacios (en un activo, `400`)."
          },
          "otro": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          }
        }
      },
      "VistaBi": {
        "type": "string",
        "enum": [
          "activos",
          "espacios",
          "ordenes",
          "incidencias",
          "preventivo",
          "lecturas",
          "sla_ot",
          "fiabilidad_activos"
        ],
        "description": "Las vistas de BI. Columnas de cada una, en su orden, en `x-talavora-columnas` (las de `x-talavora-columnas-coste` solo salen con `mantenimiento:costes`). Nombres y columnas ESTABLES: solo se añaden columnas al final.",
        "x-talavora-columnas": {
          "activos": [
            "id",
            "codigo",
            "nombre",
            "categoria_id",
            "categoria_codigo",
            "categoria_nombre",
            "espacio_id",
            "espacio_codigo",
            "espacio_nombre",
            "padre_id",
            "criticidad",
            "estado",
            "en_alta",
            "marca",
            "modelo",
            "num_serie",
            "fecha_compra",
            "instalado_el",
            "garantia_hasta",
            "coste_adquisicion",
            "proveedor_id",
            "responsable_id",
            "lat",
            "lng",
            "posicion_origen",
            "ref_ifc",
            "ref_rfid",
            "ref_inventario",
            "ref_erp",
            "ref_otro",
            "creado_at",
            "actualizado_at"
          ],
          "espacios": [
            "id",
            "codigo",
            "nombre",
            "tipo_id",
            "tipo_nombre",
            "padre_id",
            "raiz_id",
            "profundidad",
            "ruta",
            "direccion",
            "lat",
            "lng",
            "centro_id",
            "cliente_centro_id",
            "en_alta",
            "ref_ifc",
            "ref_rfid",
            "ref_inventario",
            "ref_erp",
            "ref_catastro",
            "ref_otro",
            "creado_at",
            "actualizado_at"
          ],
          "ordenes": [
            "id",
            "codigo",
            "titulo",
            "tipo_mantenimiento",
            "estado",
            "prioridad",
            "activo_id",
            "espacio_id",
            "incidencia_id",
            "plan_activo_id",
            "proveedor_id",
            "tecnico_id",
            "brigada_id",
            "modo_fallo_id",
            "causa_id",
            "accion_id",
            "creada_at",
            "fecha_prevista",
            "fecha_programada",
            "fecha_limite",
            "iniciada_at",
            "cerrada_at",
            "cancelada_at",
            "duracion_estimada_min",
            "minutos_trabajo",
            "minutos_desplazamiento",
            "minutos_espera",
            "minutos_parada",
            "horas_hasta_inicio",
            "horas_hasta_cierre",
            "vencida",
            "coste_mano",
            "coste_material",
            "coste_externo",
            "coste_total",
            "coste_incompleto",
            "actualizada_at"
          ],
          "incidencias": [
            "id",
            "codigo",
            "titulo",
            "estado",
            "prioridad",
            "origen",
            "activo_id",
            "espacio_id",
            "ot_id",
            "duplicada_de",
            "creada_at",
            "resuelta_at",
            "horas_hasta_resolver",
            "actualizada_at"
          ],
          "preventivo": [
            "id",
            "codigo",
            "plan_id",
            "plan_codigo",
            "plan_nombre",
            "reglamentario",
            "normativa",
            "linea_id",
            "activo_id",
            "espacio_id",
            "estado",
            "fecha_programada",
            "fecha_limite",
            "cerrada_at",
            "situacion",
            "dias_retraso",
            "creada_at"
          ],
          "lecturas": [
            "id",
            "contador_id",
            "contador_codigo",
            "contador_nombre",
            "contador_tipo",
            "unidad",
            "activo_id",
            "valor",
            "leido_at",
            "origen",
            "reinicio",
            "ot_id",
            "creada_at"
          ],
          "sla_ot": [
            "ot_id",
            "codigo",
            "titulo",
            "proveedor_id",
            "contrato_id",
            "contrato_nombre",
            "horario",
            "prioridad",
            "estado_ot",
            "origen",
            "espacio_id",
            "referencia_at",
            "respuesta_at",
            "resolucion_at",
            "plazo_respuesta_min",
            "plazo_resolucion_min",
            "limite_respuesta_at",
            "limite_resolucion_at",
            "usados_respuesta_min",
            "usados_resolucion_min",
            "estado_respuesta",
            "estado_resolucion",
            "estado",
            "pausado_min",
            "retraso_respuesta_min",
            "retraso_resolucion_min",
            "penalizacion"
          ],
          "fiabilidad_activos": [
            "activo_id",
            "codigo",
            "nombre",
            "categoria_id",
            "espacio_id",
            "criticidad",
            "desde",
            "hasta",
            "dias",
            "minutos_periodo",
            "minutos_parada",
            "fallos",
            "disponibilidad_pct",
            "mtbf_h",
            "mttr_h",
            "correctivas_cerradas"
          ]
        },
        "x-talavora-columnas-coste": {
          "activos": [
            "coste_adquisicion"
          ],
          "ordenes": [
            "coste_mano",
            "coste_material",
            "coste_externo",
            "coste_total",
            "coste_incompleto"
          ],
          "sla_ot": [
            "penalizacion"
          ]
        },
        "x-talavora-clave": {
          "activos": "id",
          "espacios": "id",
          "ordenes": "id",
          "incidencias": "id",
          "preventivo": "id",
          "lecturas": "id",
          "sla_ot": "ot_id",
          "fiabilidad_activos": "activo_id"
        }
      },
      "LecturaDispositivo": {
        "type": "object",
        "required": [
          "punto",
          "valor"
        ],
        "properties": {
          "punto": {
            "type": "string",
            "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/#-]{0,79}$",
            "description": "El código del canal en el aparato (`AI1`, `ahu1.temp_impulsion`, `40001`…), mapeado a un contador en Talavora.",
            "examples": [
              "AI1"
            ]
          },
          "valor": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string",
                "pattern": "^-?\\d+(\\.\\d+)?$"
              }
            ],
            "description": "Valor BRUTO del aparato (número, o texto con punto decimal). Talavora lo convierte con el factor y el desplazamiento del punto.",
            "examples": [
              2931
            ]
          },
          "leido_at": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "number"
              }
            ],
            "description": "ISO 8601 CON zona (`Z` o `+02:00`) o epoch en segundos (o milisegundos si pasa de 1e11). Sin él, la hora de llegada. Hasta 30 días atrás y 5 minutos adelante.",
            "examples": [
              "2026-10-29T09:15:00Z"
            ]
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Opcional: vuelve en el `detalle` para casar la respuesta. No interviene en la idempotencia."
          }
        }
      },
      "LoteLecturasDispositivo": {
        "description": "Un lote: `{ \"lecturas\": [...] }` (con `version` opcional) o la lista sola. De 1 a 500 lecturas.",
        "oneOf": [
          {
            "type": "object",
            "required": [
              "lecturas"
            ],
            "properties": {
              "version": {
                "type": "string",
                "maxLength": 60,
                "description": "Versión del firmware o del flujo (como `X-Talavora-Version`)."
              },
              "lecturas": {
                "type": "array",
                "minItems": 1,
                "maxItems": 500,
                "items": {
                  "$ref": "#/components/schemas/LecturaDispositivo"
                }
              }
            }
          },
          {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "$ref": "#/components/schemas/LecturaDispositivo"
            }
          }
        ],
        "examples": [
          {
            "version": "nodered-3.1",
            "lecturas": [
              {
                "punto": "AI1",
                "valor": 2931,
                "leido_at": "2026-10-29T09:15:00Z"
              },
              {
                "punto": "HORAS",
                "valor": 1520.5,
                "leido_at": 1793000000
              }
            ]
          }
        ]
      },
      "RespuestaLecturasDispositivo": {
        "type": "object",
        "required": [
          "ok",
          "recibidas",
          "aceptadas",
          "rechazadas",
          "detalle"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "recibidas": {
            "type": "integer",
            "description": "Lecturas del lote."
          },
          "aceptadas": {
            "type": "integer",
            "description": "Agregadas + guardadas + repetidas."
          },
          "rechazadas": {
            "type": "integer"
          },
          "guardadas": {
            "type": "integer",
            "description": "Lecturas que han quedado en Talavora con esta petición (intervalos cerrados y atrasadas)."
          },
          "disparos": {
            "type": "integer",
            "description": "Umbrales que han saltado."
          },
          "cupo": {
            "type": "object",
            "properties": {
              "por_minuto": {
                "type": "integer"
              },
              "restante": {
                "type": "integer"
              }
            }
          },
          "hora_servidor": {
            "type": "string",
            "format": "date-time"
          },
          "detalle": {
            "type": "array",
            "description": "Una entrada por lectura del lote, en su orden.",
            "items": {
              "type": "object",
              "required": [
                "i",
                "estado"
              ],
              "properties": {
                "i": {
                  "type": "integer",
                  "description": "Posición en el lote (desde 0)."
                },
                "id": {
                  "type": "string",
                  "format": "uuid",
                  "description": "El `id` que mandó la pasarela, si lo mandó."
                },
                "estado": {
                  "type": "string",
                  "enum": [
                    "agregada",
                    "guardada",
                    "repetida",
                    "rechazada"
                  ],
                  "description": "`agregada`: entra en el intervalo abierto; `guardada`: una atrasada que se guarda sola; `repetida`: ya estaba (reenvío); `rechazada`: con su `code` (no reintentarla)."
                },
                "code": {
                  "type": "string",
                  "pattern": "^MNT_[A-Z_]+$",
                  "description": "Solo en las rechazadas: `MNT_IOT_FORMATO`, `MNT_IOT_PUNTO`, `MNT_IOT_PUNTO_BAJA`, `MNT_IOT_ANTIGUA`, `MNT_CONTADOR_BAJA`, `MNT_LECTURA_VALOR`, `MNT_LECTURA_FUTURA`, `MNT_LECTURA_BAJA`, `MNT_LECTURA_SUPERA`, `MNT_LECTURA_NEGATIVA` (u otro `MNT_…` de las reglas de los contadores)."
                },
                "error": {
                  "type": "string",
                  "description": "Texto, en las que rechaza la API antes de llegar a la base (forma, valor, hora)."
                }
              }
            }
          }
        }
      },
      "LatidoDispositivo": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "maxLength": 60,
            "description": "Versión del firmware o del flujo (si no, la de `X-Talavora-Version`)."
          },
          "hora": {
            "oneOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "number"
              }
            ],
            "description": "La hora del aparato (ISO 8601 con zona o epoch): Talavora devuelve el desvío de su reloj.",
            "examples": [
              "2026-10-29T09:15:00Z"
            ]
          }
        }
      },
      "RespuestaLatidoDispositivo": {
        "type": "object",
        "required": [
          "ok",
          "hora_servidor",
          "cupo_minuto",
          "max_lecturas_peticion",
          "puntos"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "hora_servidor": {
            "type": "string",
            "format": "date-time"
          },
          "desvio_s": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Segundos que va adelantado (+) o atrasado (−) el reloj del aparato; `null` si no mandó `hora`."
          },
          "cupo_minuto": {
            "type": "integer",
            "description": "Su cupo de lecturas por minuto."
          },
          "max_lecturas_peticion": {
            "type": "integer",
            "const": 500
          },
          "puntos": {
            "type": "array",
            "description": "Los puntos activos que debe mandar.",
            "items": {
              "type": "object",
              "properties": {
                "codigo": {
                  "type": "string"
                },
                "intervalo_min": {
                  "type": "integer",
                  "description": "Minutos del intervalo en que se agrega (15 a 1440)."
                }
              }
            }
          }
        }
      }
    }
  }
}
