{
    "openapi": "3.0.3",
    "info": {
        "title": "WIFICOR ISP Developer API",
        "version": "1.0.0",
        "description": "API para integrar tu sistema, CRM o bot con **WIFICOR ISP**: consulta clientes, deuda y servicio, y ejecuta acciones autorizadas.\n\n## Autenticación\nCada ISP crea sus llaves en *Ajustes generales > API e integraciones* y se las entrega a cada integración. Envíala en cada petición:\n\n`Authorization: Bearer <llave>`\n\nLa llave se muestra una sola vez. Puede **rotarse** (la anterior sigue válida unas horas) y **revocarse** en cualquier momento. Esta API se usa **por ISP**: la dirección base es el dominio de cada ISP.\n\n## Permisos (scopes)\nCada llave lleva scopes. Lo que puede hacer es la intersección entre sus scopes y los permisos del perfil del usuario a cuyo nombre actúa.\n\n| Scope | Permite | Estado |\n|---|---|---|\n| `customers:read` | Clientes: consultar | Disponible |\n| `billing:read` | Facturas y deuda | Disponible |\n| `payments:read` | Medios de pago | Disponible |\n| `payments:write` | Pagos: promesas y comprobantes | Disponible |\n| `payments:register` | Pagos: registrar cobros | Disponible |\n| `service:read` | Estado del servicio | Disponible |\n| `service:activate` | Reactivar servicio | Disponible |\n| `service:suspend` | Cortar servicio | Disponible |\n| `tickets:read` | Tickets: consultar | Disponible |\n| `tickets:write` | Tickets: crear | Disponible |\n| `tickets:close` | Tickets: cerrar | Disponible |\n| `network:read` | Consumo de red | Disponible |\n| `onu:read` | ONU y OLT: consultar | Disponible |\n| `onu:write` | ONU: reiniciar | Disponible |\n\n## Límites\nPor defecto **120 peticiones por minuto** por llave (el ISP puede fijar otro). Al pasarlo: `429` con `Retry-After`.\n\n## Idempotencia\nLas escrituras (crear y cerrar ticket, promesa de pago, cortar y reactivar servicio, reiniciar ONU) aceptan `Idempotency-Key`: repetir la misma llave devuelve la respuesta original sin volver a ejecutar.\n\n## Errores\nSiempre `{ \"error\": \"CODIGO\", \"message\": \"...\" }`. Programa contra `error`, no contra `message`.\n\n## Webhooks\nEn vez de preguntar cada rato, registra una dirección **https** en *Ajustes generales > Webhooks* y WIFICOR ISP te avisa por sí solo cuando pasa algo: un pago, una factura, un ticket, un cambio de servicio o de cliente, o una ONU caída. Cada aviso es un `POST` con un cuerpo JSON (`id`, `type`, `apiVersion`, `createdAt`, `data`) y una firma. Responde **2xx** rápido (procesa después): cualquier otra respuesta, o no responder en 8 segundos, se reintenta.\n\nLos avisos se detectan cada minuto (las caídas de ONU, cada 5 minutos, según el monitor de la OLT). Se entregan **al menos una vez**: guarda el `id` de cada evento y descarta los repetidos. No se garantiza el orden entre eventos distintos.\n\nPor seguridad solo se aceptan direcciones https públicas (puerto 443 u 8443): nunca una red interna.\n\n## Verificar la firma\nCada entrega lleva `X-Wificor-Signature: t=<unix>,v1=<hex>`. Para comprobar que viene de WIFICOR ISP: toma el cuerpo **exacto** tal como llegó, calcula `HMAC-SHA256` de `t + \".\" + cuerpo` con el secreto de tu webhook (se muestra una sola vez al crearlo), compáralo con `v1` en tiempo constante y rechaza la entrega si `t` se aleja más de 5 minutos de tu reloj (así no se puede reenviar una captura vieja).\n\n## Reintentos\nSi tu servidor no responde 2xx, se reintenta a **1 minuto, 5 minutos, 30 minutos, 2 horas y 6 horas**; después la entrega se da por fallida. Si un webhook acumula 30 fallas seguidas se apaga solo y el administrador lo ve en el panel.\n\n## Multi-país\nLa moneda, la zona horaria y el país los fija cada empresa; `GET /ping` los devuelve. Los importes llevan su moneda (ISO 4217).\n\n## Compatibilidad\n`/api/v1` y `/connector/v1` son la misma API. Los cambios dentro de v1 solo agregan campos y funciones; nada se quita ni cambia de significado sin una nueva versión.",
        "contact": {
            "name": "WIFICOR",
            "url": "https://wificorcloud.com/"
        },
        "x-api-version": "v1",
        "x-release": "8.0.1"
    },
    "servers": [
        {
            "url": "https://{dominio}/api/v1",
            "description": "Tu sistema WIFICOR ISP",
            "variables": {
                "dominio": {
                    "default": "tu-dominio.com",
                    "description": "El dominio de tu sistema WIFICOR ISP."
                }
            }
        }
    ],
    "tags": [
        {
            "name": "Sistema",
            "description": "Estado de la API y de la llave que consulta."
        },
        {
            "name": "Clientes",
            "description": "Buscar al cliente por su celular y ver su ficha."
        },
        {
            "name": "Facturación",
            "description": "Deuda y facturas del cliente."
        },
        {
            "name": "Pagos",
            "description": "Medios de pago, promesas de pago y comprobantes."
        },
        {
            "name": "Servicio",
            "description": "Estado del servicio y su corte o reactivación."
        },
        {
            "name": "Tickets",
            "description": "Tickets de soporte del cliente."
        },
        {
            "name": "Red",
            "description": "Consumo, routers y conexión del cliente."
        },
        {
            "name": "ONU y OLT",
            "description": "ONU del cliente y OLT de la empresa (el último estado que dejó el monitor)."
        }
    ],
    "security": [
        {
            "bearerAuth": []
        }
    ],
    "paths": {
        "/openapi": {
            "get": {
                "operationId": "openapi",
                "tags": [
                    "Sistema"
                ],
                "summary": "Esta especificación (OpenAPI)",
                "description": "La especificación de ESTE sistema, generada en el momento, con su dirección ya puesta. No necesita llave.",
                "security": [],
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "description": "Documento OpenAPI 3.0."
                                },
                                "example": {
                                    "openapi": "3.0.3"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/ping": {
            "get": {
                "operationId": "ping",
                "tags": [
                    "Sistema"
                ],
                "summary": "Estado de la API y de tu llave",
                "description": "Comprueba la conexión y devuelve la región de la empresa (país, moneda, zona horaria) y **solo las funciones que esta llave puede usar**. Úsalo para probar la llave.",
                "x-required-scope": null,
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Ping"
                                },
                                "example": {
                                    "ok": true,
                                    "system": {
                                        "name": "Wificor ISP",
                                        "version": "8.0.1"
                                    },
                                    "apiVersion": "v1",
                                    "country": "PE",
                                    "currency": "PEN",
                                    "timezone": "America/Lima",
                                    "language": "es",
                                    "capabilities": [
                                        "customer.lookup",
                                        "customer.detail",
                                        "invoices"
                                    ],
                                    "scopes": [
                                        "customers:read",
                                        "billing:read"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/payment-methods": {
            "get": {
                "operationId": "paymentMethods",
                "tags": [
                    "Pagos"
                ],
                "summary": "Medios de pago de la empresa",
                "description": "Las cuentas de cobro activas del país de la empresa (cada banco o billetera con su nombre).\n\nPermiso (scope) requerido: `payments:read`.",
                "x-required-scope": "payments:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaymentMethods"
                                },
                                "example": {
                                    "accounts": [
                                        {
                                            "bank": "Yape",
                                            "holder": "Mi Empresa SAC",
                                            "number": "987654321",
                                            "cci": null,
                                            "phone": "51987654321",
                                            "currency": "PEN",
                                            "note": null
                                        }
                                    ],
                                    "instructions": "Cuando pagues, envíanos la captura del comprobante por este chat."
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/lookup": {
            "get": {
                "operationId": "lookup",
                "tags": [
                    "Clientes"
                ],
                "summary": "Buscar clientes por celular",
                "description": "Devuelve los clientes que tienen ese celular. **Con más de uno el número es ambiguo: no adivines.** `numberBlocked` indica si el número está en la lista de números a los que este sistema no atiende.\n\nPermiso (scope) requerido: `customers:read`.",
                "parameters": [
                    {
                        "name": "phone",
                        "in": "query",
                        "required": true,
                        "description": "Celular con código de país, sin +, de 7 a 15 dígitos.",
                        "schema": {
                            "type": "string",
                            "example": "51987654321"
                        }
                    }
                ],
                "x-required-scope": "customers:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/LookupResult"
                                },
                                "example": {
                                    "matches": [
                                        {
                                            "id": "1234",
                                            "name": "Ana Pérez",
                                            "phones": [
                                                "51987654321"
                                            ],
                                            "document": "4****678",
                                            "status": "active",
                                            "plan": "Fibra 100",
                                            "balance": {
                                                "amount": 80,
                                                "currency": "PEN",
                                                "overdueInvoices": 1
                                            },
                                            "nextDueDate": "2026-10-15",
                                            "zone": "Zona Norte"
                                        }
                                    ],
                                    "numberBlocked": false
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: `phone` no tiene entre 7 y 15 dígitos.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}": {
            "get": {
                "operationId": "detail",
                "tags": [
                    "Clientes"
                ],
                "summary": "Ficha del cliente",
                "description": "El resumen del cliente más sus servicios, ONU, último pago y tickets abiertos. El documento siempre va enmascarado.\n\nPermiso (scope) requerido: `customers:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "x-required-scope": "customers:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CustomerDetail"
                                },
                                "example": {
                                    "id": "1234",
                                    "name": "Ana Pérez",
                                    "phones": [
                                        "51987654321"
                                    ],
                                    "document": "4****678",
                                    "status": "active",
                                    "plan": "Fibra 100",
                                    "balance": {
                                        "amount": 80,
                                        "currency": "PEN",
                                        "overdueInvoices": 1
                                    },
                                    "nextDueDate": "2026-10-15",
                                    "zone": "Zona Norte",
                                    "address": "Av. Principal 123",
                                    "services": [
                                        {
                                            "plan": "Fibra 100",
                                            "speed": 100,
                                            "speedUnit": "Mbps",
                                            "monthlyPrice": 30,
                                            "technology": "Fibra",
                                            "status": "active"
                                        }
                                    ],
                                    "onu": {
                                        "state": "online",
                                        "lastSeen": "2026-10-06 10:40:00"
                                    },
                                    "lastPayment": {
                                        "date": "2026-09-15",
                                        "amount": 30
                                    },
                                    "openTickets": 0,
                                    "acquiredAt": "2024-03-02"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/invoices": {
            "get": {
                "operationId": "invoices",
                "tags": [
                    "Facturación"
                ],
                "summary": "Facturas del cliente",
                "description": "Facturas del cliente, la de vencimiento más reciente primero.\n\nPermiso (scope) requerido: `billing:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "required": false,
                        "description": "Qué facturas devolver.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "pending",
                                "paid",
                                "all"
                            ],
                            "default": "pending"
                        }
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "required": false,
                        "description": "Cuántas devolver (1 a 100).",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 100,
                            "default": 20
                        }
                    }
                ],
                "x-required-scope": "billing:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/InvoiceList"
                                },
                                "example": {
                                    "invoices": [
                                        {
                                            "id": "9001",
                                            "number": "F001-0000123",
                                            "issuedAt": "2026-10-01",
                                            "dueDate": "2026-10-15",
                                            "amount": 80,
                                            "paid": 0,
                                            "remaining": 80,
                                            "currency": "PEN",
                                            "status": "pending",
                                            "description": "Mensualidad octubre"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/tickets": {
            "get": {
                "operationId": "ticketsList",
                "tags": [
                    "Tickets"
                ],
                "summary": "Tickets del cliente",
                "description": "Hasta 50 tickets, el más reciente primero.\n\nPermiso (scope) requerido: `tickets:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "required": false,
                        "description": "Qué tickets devolver.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "open",
                                "closed",
                                "all"
                            ],
                            "default": "open"
                        }
                    }
                ],
                "x-required-scope": "tickets:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TicketList"
                                },
                                "example": {
                                    "tickets": [
                                        {
                                            "id": "3021",
                                            "number": "T-3021",
                                            "type": "AVERIA INTERNET",
                                            "status": "open",
                                            "createdAt": "2026-10-06 09:12:00",
                                            "summary": "Sin internet desde la mañana"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "ticketCreate",
                "tags": [
                    "Tickets"
                ],
                "summary": "Crear un ticket",
                "description": "Crea el ticket con la misma lógica del panel (incluye el aviso al equipo). Repetir la misma `Idempotency-Key` (máximo 100 caracteres) devuelve la respuesta original SIN volver a ejecutar la operación; se recuerda 24 horas.\n\nPermiso (scope) requerido: `tickets:write`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Evita duplicar la operación si reintentas. Máximo 100 caracteres.",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/TicketCreate"
                            },
                            "example": {
                                "type": "outage",
                                "description": "Sin internet desde la mañana"
                            }
                        }
                    }
                },
                "x-required-scope": "tickets:write",
                "x-idempotent": true,
                "responses": {
                    "201": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TicketCreated"
                                },
                                "example": {
                                    "id": "3021",
                                    "number": "T-3021",
                                    "status": "open"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: falta `description` o pasa de 500 caracteres.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: no hay motivos de ticket configurados, o el cliente ya tiene un ticket a esa hora.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/payment-promises": {
            "post": {
                "operationId": "promiseCreate",
                "tags": [
                    "Pagos"
                ],
                "summary": "Registrar una promesa de pago",
                "description": "Registra el compromiso con **exactamente la lógica del panel**: fechas, candado de penalidad, estado de compromiso y liberación del corte. Repetir la misma `Idempotency-Key` (máximo 100 caracteres) devuelve la respuesta original SIN volver a ejecutar la operación; se recuerda 24 horas.\n\nPermiso (scope) requerido: `payments:write`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Evita duplicar la operación si reintentas. Máximo 100 caracteres.",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/PromiseCreate"
                            },
                            "example": {
                                "promisedDate": "2026-10-20",
                                "note": "Cobra el viernes"
                            }
                        }
                    }
                },
                "x-required-scope": "payments:write",
                "x-idempotent": true,
                "responses": {
                    "201": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PromiseCreated"
                                },
                                "example": {
                                    "id": "55",
                                    "promisedDate": "2026-10-20",
                                    "serviceStatus": "active"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: `promisedDate` no tiene el formato AAAA-MM-DD.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe o la factura no es de este cliente.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: el cliente no tiene contrato, o el sistema rechazó la promesa (el mensaje dice por qué).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/payment-proofs": {
            "post": {
                "operationId": "proofReceive",
                "tags": [
                    "Pagos"
                ],
                "summary": "Entregar un comprobante de pago",
                "description": "Entrega la imagen o PDF a **Cobros IA**, que lo lee y lo concilia. Responde `202` de inmediato; el resultado (y el aviso al cliente) llegan después por los canales del sistema. Requiere que Cobros IA esté activado.\n\nPermiso (scope) requerido: `payments:write`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ProofCreate"
                            },
                            "example": {
                                "fileUrl": "https://ejemplo.com/comprobante.jpg",
                                "mimeType": "image/jpeg"
                            }
                        }
                    }
                },
                "x-required-scope": "payments:write",
                "responses": {
                    "202": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ProofReceived"
                                },
                                "example": {
                                    "id": "cap-1a2b3c4d5e6f",
                                    "status": "received"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: `fileUrl` no es una dirección https.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: Cobros IA no está activado, o el cliente no tiene celular registrado.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/service": {
            "get": {
                "operationId": "serviceStatus",
                "tags": [
                    "Servicio"
                ],
                "summary": "Estado del servicio",
                "description": "Contrato, conexión (en línea o no), ONU y potencia óptica. Lo deja el monitor de la OLT y el colector de tráfico: no consulta el equipo en vivo.\n\nPermiso (scope) requerido: `service:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "x-required-scope": "service:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ServiceStatus"
                                },
                                "example": {
                                    "status": "active",
                                    "suspendedSince": null,
                                    "plan": "Fibra 100",
                                    "speed": 100,
                                    "speedUnit": "Mbps",
                                    "technology": "Fibra",
                                    "router": "RB-Norte",
                                    "connection": {
                                        "state": "online",
                                        "lastSeen": "2026-10-06 10:40:00"
                                    },
                                    "onu": {
                                        "serial": "TPLG12345678",
                                        "olt": "OLT-1",
                                        "pon": "1/1/3",
                                        "state": "online",
                                        "cause": null,
                                        "rxDbm": -21.4,
                                        "rxLevel": "good",
                                        "updatedAt": "2026-10-06 10:40:00"
                                    },
                                    "actions": {
                                        "canSuspend": true,
                                        "canActivate": false
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/traffic": {
            "get": {
                "operationId": "traffic",
                "tags": [
                    "Red"
                ],
                "summary": "Consumo del cliente",
                "description": "Descarga y subida por hora (24h) o por día (7d, 30d), con totales y el pico.\n\nPermiso (scope) requerido: `network:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "range",
                        "in": "query",
                        "required": false,
                        "description": "Periodo.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "24h",
                                "7d",
                                "30d"
                            ],
                            "default": "24h"
                        }
                    }
                ],
                "x-required-scope": "network:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Traffic"
                                },
                                "example": {
                                    "range": "24h",
                                    "unit": "hour",
                                    "monitored": true,
                                    "lastTrafficAt": "2026-10-06 10:55:00",
                                    "totals": {
                                        "downloadBytes": 120000000,
                                        "uploadBytes": 8000000
                                    },
                                    "peak": {
                                        "at": "2026-10-06 09:00",
                                        "downloadBytes": 120000000
                                    },
                                    "points": [
                                        {
                                            "at": "2026-10-06 09:00",
                                            "downloadBytes": 120000000,
                                            "uploadBytes": 8000000
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: el cliente no tiene contrato.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/service/suspend": {
            "post": {
                "operationId": "serviceSuspend",
                "tags": [
                    "Servicio"
                ],
                "summary": "Cortar el servicio",
                "description": "Corta el servicio con **los mismos pasos que el botón Cortar del panel** (estado del contrato, instalación y corte en el router). Es una acción sobre la red del cliente. Si ya estaba cortado no hace nada (`changed: false`). Repetir la misma `Idempotency-Key` (máximo 100 caracteres) devuelve la respuesta original SIN volver a ejecutar la operación; se recuerda 24 horas.\n\nPermiso (scope) requerido: `service:suspend`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Evita duplicar la operación si reintentas. Máximo 100 caracteres.",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        }
                    }
                ],
                "x-required-scope": "service:suspend",
                "x-idempotent": true,
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ServiceChange"
                                },
                                "example": {
                                    "status": "suspended",
                                    "changed": true,
                                    "network": {
                                        "applied": true,
                                        "note": null
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: el cliente no tiene contrato, o el servicio está en instalación o dado de baja.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/service/activate": {
            "post": {
                "operationId": "serviceActivate",
                "tags": [
                    "Servicio"
                ],
                "summary": "Reactivar el servicio",
                "description": "Reactiva un servicio **cortado** con los mismos pasos que el botón Activar del panel. Una baja o una instalación no se reactivan por aquí. Si ya estaba activo no hace nada. Repetir la misma `Idempotency-Key` (máximo 100 caracteres) devuelve la respuesta original SIN volver a ejecutar la operación; se recuerda 24 horas.\n\nPermiso (scope) requerido: `service:activate`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Evita duplicar la operación si reintentas. Máximo 100 caracteres.",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        }
                    }
                ],
                "x-required-scope": "service:activate",
                "x-idempotent": true,
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ServiceChange"
                                },
                                "example": {
                                    "status": "active",
                                    "changed": true,
                                    "network": {
                                        "applied": true,
                                        "note": null
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: el cliente no tiene contrato, o solo se reactiva un servicio cortado.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers": {
            "get": {
                "operationId": "customersList",
                "tags": [
                    "Clientes"
                ],
                "summary": "Listar clientes",
                "description": "Los clientes de la empresa, de a pocos y por orden de id, cada uno con el mismo resumen de la búsqueda por celular. Pagina con `nextCursor` → `after`. Con `q` busca por nombre, documento, correo o celular (con o sin código de país).\n\nPermiso (scope) requerido: `customers:read`.",
                "parameters": [
                    {
                        "name": "q",
                        "in": "query",
                        "required": false,
                        "description": "Texto a buscar: nombre, documento, correo o celular.",
                        "schema": {
                            "type": "string",
                            "example": "ana"
                        }
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "required": false,
                        "description": "Solo clientes en ese estado del servicio.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "active",
                                "suspended",
                                "pending",
                                "retired"
                            ]
                        }
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "required": false,
                        "description": "Cuántos devolver (1 a 100).",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 100,
                            "default": 25
                        }
                    },
                    {
                        "name": "after",
                        "in": "query",
                        "required": false,
                        "description": "El `nextCursor` de la página anterior.",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "x-required-scope": "customers:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CustomerPage"
                                },
                                "example": {
                                    "customers": [
                                        {
                                            "id": "1234",
                                            "name": "Ana Pérez",
                                            "phones": [
                                                "51987654321"
                                            ],
                                            "document": "4****678",
                                            "status": "active",
                                            "plan": "Fibra 100",
                                            "balance": {
                                                "amount": 80,
                                                "currency": "PEN",
                                                "overdueInvoices": 1
                                            },
                                            "nextDueDate": "2026-10-15",
                                            "zone": "Zona Norte"
                                        }
                                    ],
                                    "nextCursor": "1234"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: `status` o `after` no son válidos.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/invoices/{invoiceId}": {
            "get": {
                "operationId": "invoiceDetail",
                "tags": [
                    "Facturación"
                ],
                "summary": "Detalle de una factura",
                "description": "La factura completa de un cliente: totales, impuesto, periodo que cubre, líneas y los pagos aplicados. La factura debe ser de ese cliente.\n\nPermiso (scope) requerido: `billing:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "invoiceId",
                        "in": "path",
                        "required": true,
                        "description": "Id de la factura (el que devuelve la lista de facturas).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "9001"
                        }
                    }
                ],
                "x-required-scope": "billing:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/InvoiceDetail"
                                },
                                "example": {
                                    "id": "9001",
                                    "number": "F001-0000123",
                                    "issuedAt": "2026-10-01",
                                    "dueDate": "2026-10-15",
                                    "amount": 80,
                                    "paid": 0,
                                    "remaining": 80,
                                    "currency": "PEN",
                                    "status": "pending",
                                    "description": "Mensualidad octubre",
                                    "period": {
                                        "from": "2026-10-01",
                                        "to": "2026-10-31"
                                    },
                                    "subtotal": 80,
                                    "discount": 0,
                                    "tax": {
                                        "amount": 0,
                                        "percent": 0,
                                        "included": false
                                    },
                                    "installments": null,
                                    "lines": [
                                        {
                                            "type": "service",
                                            "description": "Servicio de internet, mes de octubre",
                                            "quantity": 1,
                                            "price": 80,
                                            "taxPercent": 0,
                                            "total": 80
                                        }
                                    ],
                                    "payments": [
                                        {
                                            "id": "5501",
                                            "invoiceId": "9001",
                                            "invoiceNumber": "F001-0000123",
                                            "date": "2026-10-05 14:30:00",
                                            "amount": 30,
                                            "currency": "PEN",
                                            "method": "YAPE",
                                            "reference": "00845122",
                                            "status": "valid"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente o la factura no existen (o la factura no es de ese cliente).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/payments": {
            "get": {
                "operationId": "customerPayments",
                "tags": [
                    "Pagos"
                ],
                "summary": "Pagos de un cliente",
                "description": "Los pagos del cliente entre dos fechas (por defecto los últimos 30 días, hasta 93), el más reciente primero. Un pago anulado no se borra: sale con `status: void`.\n\nPermiso (scope) requerido: `payments:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "from",
                        "in": "query",
                        "required": false,
                        "description": "Desde (AAAA-MM-DD). Por defecto, 30 días antes de `to`.",
                        "schema": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-10-01"
                        }
                    },
                    {
                        "name": "to",
                        "in": "query",
                        "required": false,
                        "description": "Hasta (AAAA-MM-DD, inclusive). Por defecto, hoy. El rango máximo es de 93 días.",
                        "schema": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-10-31"
                        }
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "required": false,
                        "description": "Pagos válidos, anulados o todos.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "valid",
                                "void",
                                "all"
                            ],
                            "default": "valid"
                        }
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "required": false,
                        "description": "Cuántos devolver (1 a 100).",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 100,
                            "default": 25
                        }
                    },
                    {
                        "name": "after",
                        "in": "query",
                        "required": false,
                        "description": "El `nextCursor` de la página anterior.",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "5501"
                        }
                    }
                ],
                "x-required-scope": "payments:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaymentList"
                                },
                                "example": {
                                    "payments": [
                                        {
                                            "id": "5501",
                                            "invoiceId": "9001",
                                            "invoiceNumber": "F001-0000123",
                                            "date": "2026-10-05 14:30:00",
                                            "amount": 30,
                                            "currency": "PEN",
                                            "method": "YAPE",
                                            "reference": "00845122",
                                            "status": "valid"
                                        }
                                    ],
                                    "nextCursor": null
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: las fechas no tienen el formato AAAA-MM-DD, el rango pasa de 93 días, o `status` / `after` no son válidos.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "paymentCreate",
                "tags": [
                    "Pagos"
                ],
                "summary": "Registrar un cobro (completo o parcial)",
                "description": "Registra el cobro con **exactamente la lógica de Registrar pago del panel**: reparto en cascada entre las facturas, compromiso o abono parcial cuando el pago deja saldo (con plazo y recordatorio opcionales), factura electrónica, reactivación del servicio y aviso. **Mueve dinero y estados de cuenta**: exige el permiso propio `payments:register`, que las llaves anteriores no heredan. Repetir la misma `Idempotency-Key` (máximo 100 caracteres) devuelve la respuesta original SIN volver a ejecutar la operación; se recuerda 24 horas.\n\nPermiso (scope) requerido: `payments:register`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Evita duplicar la operación si reintentas. Máximo 100 caracteres.",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/PaymentCreate"
                            },
                            "example": {
                                "invoiceIds": [
                                    "9001"
                                ],
                                "amount": 40,
                                "methodId": "2",
                                "operation": "00123456",
                                "comment": "Pagó por Yape",
                                "activateService": true,
                                "notifyCustomer": false,
                                "balance": {
                                    "deadline": "2026-10-25",
                                    "deadlineTime": "18:00"
                                }
                            }
                        }
                    }
                },
                "x-required-scope": "payments:register",
                "x-idempotent": true,
                "responses": {
                    "201": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaymentCreated"
                                },
                                "example": {
                                    "registered": true,
                                    "amount": 40,
                                    "invoiceIds": [
                                        "9001"
                                    ],
                                    "message": "Se ha actualizado el registro exitosamente.",
                                    "balance": {
                                        "amount": 40,
                                        "currency": "PEN",
                                        "overdueInvoices": 0
                                    },
                                    "serviceStatus": "active"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: falta `amount`, `invoiceIds` o `methodId`, o una fecha u hora tiene mal el formato.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe, o una factura no es suya o ya no está pendiente.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: el monto pasa de lo que se debe, o el sistema rechazó el cobro (el mensaje dice por qué).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/payments": {
            "get": {
                "operationId": "paymentsList",
                "tags": [
                    "Pagos"
                ],
                "summary": "Consulta de pagos de la empresa",
                "description": "Los pagos de TODOS los clientes entre dos fechas (por defecto los últimos 30 días, hasta 93), el más reciente primero. Sirve para conciliar. Cada pago trae su cliente.\n\nPermiso (scope) requerido: `payments:read`.",
                "parameters": [
                    {
                        "name": "from",
                        "in": "query",
                        "required": false,
                        "description": "Desde (AAAA-MM-DD). Por defecto, 30 días antes de `to`.",
                        "schema": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-10-01"
                        }
                    },
                    {
                        "name": "to",
                        "in": "query",
                        "required": false,
                        "description": "Hasta (AAAA-MM-DD, inclusive). Por defecto, hoy. El rango máximo es de 93 días.",
                        "schema": {
                            "type": "string",
                            "format": "date",
                            "example": "2026-10-31"
                        }
                    },
                    {
                        "name": "status",
                        "in": "query",
                        "required": false,
                        "description": "Pagos válidos, anulados o todos.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "valid",
                                "void",
                                "all"
                            ],
                            "default": "valid"
                        }
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "required": false,
                        "description": "Cuántos devolver (1 a 100).",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 100,
                            "default": 25
                        }
                    },
                    {
                        "name": "after",
                        "in": "query",
                        "required": false,
                        "description": "El `nextCursor` de la página anterior.",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "5501"
                        }
                    }
                ],
                "x-required-scope": "payments:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaymentReport"
                                },
                                "example": {
                                    "payments": [
                                        {
                                            "id": "5501",
                                            "invoiceId": "9001",
                                            "invoiceNumber": "F001-0000123",
                                            "date": "2026-10-05 14:30:00",
                                            "amount": 30,
                                            "currency": "PEN",
                                            "method": "YAPE",
                                            "reference": "00845122",
                                            "status": "valid",
                                            "customer": {
                                                "id": "1234",
                                                "name": "Ana Pérez"
                                            }
                                        }
                                    ],
                                    "nextCursor": "5501"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: las fechas no tienen el formato AAAA-MM-DD, el rango pasa de 93 días, o `status` / `after` no son válidos.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/tickets/{ticketId}": {
            "get": {
                "operationId": "ticketDetail",
                "tags": [
                    "Tickets"
                ],
                "summary": "Detalle de un ticket",
                "description": "El ticket completo de un cliente: tipo, etapa, prioridad, fechas, técnico asignado y la solución que registró al cerrarlo. El ticket debe ser de ese cliente.\n\nPermiso (scope) requerido: `tickets:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "ticketId",
                        "in": "path",
                        "required": true,
                        "description": "Id del ticket (el que devuelve la lista de tickets).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "3021"
                        }
                    }
                ],
                "x-required-scope": "tickets:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TicketDetail"
                                },
                                "example": {
                                    "id": "3021",
                                    "number": "T-3021",
                                    "type": "AVERIA INTERNET",
                                    "status": "closed",
                                    "stage": "resolved",
                                    "priority": "high",
                                    "description": "Sin internet desde la mañana",
                                    "scheduledAt": "2026-10-06 09:12:00",
                                    "openedAt": "2026-10-06 10:00:00",
                                    "closedAt": "2026-10-06 11:30:00",
                                    "createdAt": "2026-10-06 09:12:00",
                                    "technician": "Luis Rojas",
                                    "resolution": {
                                        "comment": "Se cambió el conector",
                                        "closedAt": "2026-10-06 11:30:00"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente o el ticket no existen (o el ticket no es de ese cliente).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/network": {
            "get": {
                "operationId": "customerNetwork",
                "tags": [
                    "Red"
                ],
                "summary": "Conexión del cliente (router y PPPoE)",
                "description": "Por cada servicio activo del cliente: su router, el usuario y perfil PPPoE (**la clave nunca se entrega**), la IP asignada y la cola.\n\nPermiso (scope) requerido: `network:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "x-required-scope": "network:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CustomerNetwork"
                                },
                                "example": {
                                    "services": [
                                        {
                                            "plan": "Fibra 100",
                                            "speed": 100,
                                            "speedUnit": "MBPS",
                                            "technology": "FIBRA",
                                            "connectionType": "internet",
                                            "router": {
                                                "id": "1",
                                                "name": "RB-Norte"
                                            },
                                            "pppoe": {
                                                "user": "72194956",
                                                "profile": "100Mbps/100Mbps"
                                            },
                                            "ipAddress": "10.10.0.15",
                                            "speedLimit": null,
                                            "queue": null
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/routers": {
            "get": {
                "operationId": "routersList",
                "tags": [
                    "Red"
                ],
                "summary": "Routers de la empresa",
                "description": "Los routers con su modelo, si el sistema los gestiona, cuántos clientes tienen y el estado de su monitoreo de tráfico. **Nunca** se entregan su dirección de gestión, usuarios ni claves.\n\nPermiso (scope) requerido: `network:read`.",
                "x-required-scope": "network:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/RouterList"
                                },
                                "example": {
                                    "routers": [
                                        {
                                            "id": "1",
                                            "name": "RB-Norte",
                                            "model": "RB4011",
                                            "routerosVersion": "7.14",
                                            "managed": true,
                                            "apiEnabled": true,
                                            "customers": 120,
                                            "trafficMonitoring": {
                                                "enabled": true,
                                                "status": "activo",
                                                "lastSeen": "2026-10-06 10:55:00"
                                            }
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/onu": {
            "get": {
                "operationId": "customerOnu",
                "tags": [
                    "ONU y OLT"
                ],
                "summary": "ONU del cliente",
                "description": "La ONU vinculada al cliente y su último estado conocido, con la potencia óptica. Lo deja el monitor de la OLT: **no consulta el equipo en vivo**. `onu` es null si el cliente no tiene ONU vinculada.\n\nPermiso (scope) requerido: `onu:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "x-required-scope": "onu:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/CustomerOnu"
                                },
                                "example": {
                                    "onu": {
                                        "serial": "TPLG12345678",
                                        "olt": "OLT-1",
                                        "pon": "1/1/3",
                                        "state": "online",
                                        "cause": null,
                                        "rxDbm": -21.4,
                                        "rxLevel": "good",
                                        "updatedAt": "2026-10-06 10:40:00",
                                        "linkedAt": "2026-03-02 09:00:00"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/olts": {
            "get": {
                "operationId": "oltsList",
                "tags": [
                    "ONU y OLT"
                ],
                "summary": "OLT de la empresa",
                "description": "Las OLT con cuántas ONU tienen vinculadas a clientes y cuántas están en línea o caídas (según el último estado del monitor). **Nunca** se entregan sus conexiones (dirección, usuario, clave).\n\nPermiso (scope) requerido: `onu:read`.",
                "x-required-scope": "onu:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/OltList"
                                },
                                "example": {
                                    "olts": [
                                        {
                                            "id": "1",
                                            "name": "OLT-1",
                                            "vendor": "HUAWEI",
                                            "model": "MA5800",
                                            "zone": "Zona Norte",
                                            "onus": {
                                                "linked": 120,
                                                "online": 110,
                                                "offline": 8,
                                                "unknown": 2
                                            },
                                            "lastUpdate": "2026-10-06 10:40:00"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/tickets/{ticketId}/close": {
            "post": {
                "operationId": "ticketClose",
                "tags": [
                    "Tickets"
                ],
                "summary": "Cerrar un ticket",
                "description": "Cierra el ticket como **resuelto**, con la misma lógica del panel (solución, fecha y quién lo cerró). Si estaba pendiente, se atiende y se cierra en la misma operación. Un ticket ya resuelto no se vuelve a cerrar (`changed: false`); uno cancelado no se puede cerrar. Solo lo puede hacer una llave de un usuario **administrador**. Repetir la misma `Idempotency-Key` (máximo 100 caracteres) devuelve la respuesta original SIN volver a ejecutar la operación; se recuerda 24 horas.\n\nPermiso (scope) requerido: `tickets:close`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Evita duplicar la operación si reintentas. Máximo 100 caracteres.",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/TicketClose"
                            },
                            "example": {
                                "resolution": "Se cambió el conector",
                                "notifyCustomer": false
                            }
                        }
                    }
                },
                "x-required-scope": "tickets:close",
                "x-idempotent": true,
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/TicketClosed"
                                },
                                "example": {
                                    "id": "3021",
                                    "number": "T-3021",
                                    "status": "closed",
                                    "stage": "resolved",
                                    "changed": true,
                                    "closedAt": "2026-10-06 11:30:00"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: falta `resolution` o pasa de 500 caracteres.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente o el ticket no existen (o el ticket no es de ese cliente).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: el ticket está cancelado.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/onu/reboot": {
            "post": {
                "operationId": "customerOnuReboot",
                "tags": [
                    "ONU y OLT"
                ],
                "summary": "Reiniciar la ONU del cliente",
                "description": "Envía el reinicio a la OLT **en el acto**, con la misma lógica del Portal Cliente. Solo la ONU activa de ese cliente. **El cliente pierde la conexión unos minutos.** Por seguridad, un reinicio correcto reciente (menos de 5 minutos) de la misma ONU impide otro. Repetir la misma `Idempotency-Key` (máximo 100 caracteres) devuelve la respuesta original SIN volver a ejecutar la operación; se recuerda 24 horas.\n\nPermiso (scope) requerido: `onu:write`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    },
                    {
                        "name": "Idempotency-Key",
                        "in": "header",
                        "required": false,
                        "description": "Evita duplicar la operación si reintentas. Máximo 100 caracteres.",
                        "schema": {
                            "type": "string",
                            "maxLength": 100
                        }
                    }
                ],
                "x-required-scope": "onu:write",
                "x-idempotent": true,
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/OnuRebootResult"
                                },
                                "example": {
                                    "requested": true,
                                    "serial": "TPLG12345678",
                                    "olt": "OLT-1",
                                    "pon": "1/1/3",
                                    "note": "Reinicio enviado: el cliente pierde la conexión unos minutos."
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: el cliente no tiene ONU vinculada, su equipo no admite reinicio remoto, su servicio está dado de baja, o ya se reinició hace menos de 5 minutos.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "UNAVAILABLE: la OLT o el equipo no respondió.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/payments/options": {
            "get": {
                "operationId": "paymentOptions",
                "tags": [
                    "Pagos"
                ],
                "summary": "Qué se puede cobrar a un cliente",
                "description": "Todo lo necesario para cobrar: sus facturas pendientes (con el monto sugerido y la cuota que toca), las formas de pago activas de la empresa y su compromiso abierto. Sale de los mismos datos del módulo **Registrar pago** del panel.\n\nPermiso (scope) requerido: `payments:register`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "x-required-scope": "payments:register",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PaymentOptions"
                                },
                                "example": {
                                    "currency": "PEN",
                                    "methods": [
                                        {
                                            "id": "1",
                                            "name": "Efectivo"
                                        },
                                        {
                                            "id": "2",
                                            "name": "Yape"
                                        }
                                    ],
                                    "canRegister": true,
                                    "commitment": null,
                                    "commonAmounts": [
                                        80,
                                        100
                                    ],
                                    "invoices": [
                                        {
                                            "id": "9001",
                                            "number": "F001-0000123",
                                            "period": "octubre 2026",
                                            "description": null,
                                            "kind": "service",
                                            "amount": 80,
                                            "remaining": 80,
                                            "suggested": 80,
                                            "installment": null,
                                            "issuedAt": "2026-10-01",
                                            "dueDate": "2026-10-15",
                                            "status": "pending"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/message-templates": {
            "get": {
                "operationId": "messageTemplates",
                "tags": [
                    "Sistema"
                ],
                "summary": "Plantillas de aviso al cliente",
                "description": "El catálogo de plantillas de aviso del sistema (las que cada empresa edita en **Sistema > Plantillas**): su clave, título, familia, las variables que usan y el texto con las variables entre corchetes. Solo lectura.\n\nPermiso (scope) requerido: `customers:read`.",
                "x-required-scope": "customers:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/MessageTemplates"
                                },
                                "example": {
                                    "templates": [
                                        {
                                            "key": "pago_parcial_cliente",
                                            "title": "PAGO PARCIAL REGISTRADO",
                                            "family": "payments",
                                            "variables": [
                                                "cliente",
                                                "monto"
                                            ],
                                            "text": "Estimado(a) [cliente]: hemos recibido tu pago parcial de [monto]."
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/{id}/notifications": {
            "post": {
                "operationId": "customerNotify",
                "tags": [
                    "Facturación"
                ],
                "summary": "Aviso a pedido: recordatorio o recibo",
                "description": "Arma un aviso YA redactado con las plantillas del sistema: el **recordatorio de pago** de la factura pendiente más antigua, o el **último comprobante pagado** con su PDF. **No envía nada**: lo devuelve para que lo entregues por tu canal. (Los avisos de cobros, compromisos y tickets se piden con `notifyCustomer` o `notifyNow` en esas mismas operaciones.)\n\nPermiso (scope) requerido: `billing:read`.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "description": "Id del cliente (el que devuelve la búsqueda por celular).",
                        "schema": {
                            "type": "string",
                            "pattern": "^[0-9]+$",
                            "example": "1234"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/NotificationRequest"
                            },
                            "example": {
                                "type": "reminder"
                            }
                        }
                    }
                },
                "x-required-scope": "billing:read",
                "responses": {
                    "200": {
                        "description": "Correcto.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/NotificationResult"
                                },
                                "example": {
                                    "notifications": [
                                        {
                                            "template": "factura_recordatorio_cliente",
                                            "text": "Hola Ana, te recordamos tu factura F001-0000123 de S/ 80.00.",
                                            "variables": {
                                                "nombre_cliente": "Ana",
                                                "monto": "80.00"
                                            },
                                            "attachment": null
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "INVALID_REQUEST: `type` debe ser reminder o receipt.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "UNAUTHORIZED: falta la llave o no es válida.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "FORBIDDEN o INSUFFICIENT_SCOPE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "NOT_FOUND: el cliente no existe.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "CONFLICT: no hay facturas pendientes (recordatorio) o comprobantes pagados (recibo), o falta la plantilla.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "RATE_LIMITED.",
                        "headers": {
                            "Retry-After": {
                                "description": "Segundos de espera.",
                                "schema": {
                                    "type": "integer"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "UNAVAILABLE.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "description": "Llave de la integración: `Authorization: Bearer <llave>`."
            }
        },
        "schemas": {
            "Error": {
                "type": "object",
                "properties": {
                    "error": {
                        "type": "string",
                        "enum": [
                            "UNAUTHORIZED",
                            "FORBIDDEN",
                            "INSUFFICIENT_SCOPE",
                            "NOT_FOUND",
                            "INVALID_REQUEST",
                            "CONFLICT",
                            "RATE_LIMITED",
                            "UNAVAILABLE"
                        ],
                        "description": "Código estable: se puede programar contra él."
                    },
                    "message": {
                        "type": "string",
                        "description": "Explicación para una persona (en español). No es estable: no programes contra ella."
                    }
                },
                "required": [
                    "error",
                    "message"
                ]
            },
            "Ping": {
                "type": "object",
                "properties": {
                    "ok": {
                        "type": "boolean"
                    },
                    "system": {
                        "type": "object",
                        "properties": {
                            "name": {
                                "type": "string",
                                "example": "Wificor ISP"
                            },
                            "version": {
                                "type": "string",
                                "description": "Versión del sistema.",
                                "example": "8.0.1"
                            }
                        },
                        "required": [
                            "name",
                            "version"
                        ]
                    },
                    "apiVersion": {
                        "type": "string",
                        "example": "v1"
                    },
                    "country": {
                        "type": "string",
                        "description": "País de la empresa (ISO 3166-1 alfa-2).",
                        "example": "PE",
                        "nullable": true
                    },
                    "currency": {
                        "type": "string",
                        "description": "Moneda de la empresa (ISO 4217).",
                        "example": "PEN"
                    },
                    "timezone": {
                        "type": "string",
                        "description": "Zona horaria de la empresa.",
                        "example": "America/Lima"
                    },
                    "language": {
                        "type": "string",
                        "example": "es"
                    },
                    "capabilities": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Funciones que ESTA llave puede usar (según sus scopes y el perfil del usuario)."
                    },
                    "scopes": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Scopes de la llave."
                    }
                },
                "required": [
                    "ok",
                    "system",
                    "apiVersion",
                    "currency",
                    "timezone",
                    "capabilities",
                    "scopes"
                ]
            },
            "Balance": {
                "type": "object",
                "properties": {
                    "amount": {
                        "type": "number",
                        "description": "Deuda pendiente total.",
                        "example": 80
                    },
                    "currency": {
                        "type": "string",
                        "description": "Moneda (ISO 4217).",
                        "example": "PEN"
                    },
                    "overdueInvoices": {
                        "type": "integer",
                        "description": "Cantidad de facturas vencidas.",
                        "example": 1
                    }
                },
                "required": [
                    "amount",
                    "currency",
                    "overdueInvoices"
                ]
            },
            "Customer": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "Id del cliente (numérico, como texto).",
                        "example": "1234"
                    },
                    "name": {
                        "type": "string",
                        "example": "Ana Pérez"
                    },
                    "phones": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "example": "51987654321"
                        },
                        "description": "Celulares, solo dígitos y con código de país."
                    },
                    "document": {
                        "type": "string",
                        "description": "Documento ENMASCARADO: nunca se entrega completo.",
                        "example": "4****678"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "active",
                            "suspended",
                            "pending",
                            "retired"
                        ],
                        "description": "Estado del servicio: activo, cortado, en instalación o dado de baja."
                    },
                    "plan": {
                        "type": "string",
                        "description": "Plan contratado.",
                        "example": "Fibra 100",
                        "nullable": true
                    },
                    "balance": {
                        "$ref": "#/components/schemas/Balance"
                    },
                    "nextDueDate": {
                        "type": "string",
                        "description": "Próximo vencimiento pendiente (AAAA-MM-DD).",
                        "example": "2026-10-15",
                        "nullable": true
                    },
                    "zone": {
                        "type": "string",
                        "example": "Zona Norte",
                        "nullable": true
                    }
                },
                "required": [
                    "id",
                    "name",
                    "phones",
                    "document",
                    "status",
                    "balance"
                ]
            },
            "CustomerService": {
                "type": "object",
                "properties": {
                    "plan": {
                        "type": "string",
                        "example": "Fibra 100"
                    },
                    "speed": {
                        "description": "Velocidad contratada (número).",
                        "example": 100
                    },
                    "speedUnit": {
                        "type": "string",
                        "example": "Mbps",
                        "nullable": true
                    },
                    "monthlyPrice": {
                        "type": "number",
                        "example": 30
                    },
                    "technology": {
                        "type": "string",
                        "example": "Fibra",
                        "nullable": true
                    },
                    "status": {
                        "type": "string",
                        "example": "active"
                    }
                },
                "required": [
                    "plan",
                    "monthlyPrice",
                    "status"
                ]
            },
            "CustomerDetail": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Customer"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "address": {
                                "type": "string",
                                "example": "Av. Principal 123",
                                "nullable": true
                            },
                            "services": {
                                "type": "array",
                                "items": {
                                    "$ref": "#/components/schemas/CustomerService"
                                }
                            },
                            "onu": {
                                "type": "object",
                                "nullable": true,
                                "description": "Estado de la ONU o de la conexión; null si el sistema no lo conoce.",
                                "properties": {
                                    "state": {
                                        "type": "string",
                                        "enum": [
                                            "online",
                                            "offline",
                                            "unknown"
                                        ]
                                    },
                                    "lastSeen": {
                                        "type": "string",
                                        "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                        "example": "2026-10-06 10:40:00",
                                        "nullable": true
                                    }
                                }
                            },
                            "lastPayment": {
                                "type": "object",
                                "nullable": true,
                                "properties": {
                                    "date": {
                                        "type": "string",
                                        "example": "2026-09-15"
                                    },
                                    "amount": {
                                        "type": "number",
                                        "example": 30
                                    }
                                }
                            },
                            "openTickets": {
                                "type": "integer",
                                "description": "Tickets abiertos.",
                                "example": 0
                            },
                            "acquiredAt": {
                                "type": "string",
                                "description": "Fecha del primer contrato (AAAA-MM-DD).",
                                "example": "2024-03-02",
                                "nullable": true
                            }
                        },
                        "required": [
                            "services",
                            "openTickets"
                        ]
                    }
                ]
            },
            "LookupResult": {
                "type": "object",
                "properties": {
                    "matches": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Customer"
                        },
                        "description": "Clientes con ese celular. Si hay más de uno, el número es ambiguo: no adivines."
                    },
                    "numberBlocked": {
                        "type": "boolean",
                        "description": "El número está en la lista de números a los que este sistema no atiende (Cobros IA)."
                    }
                },
                "required": [
                    "matches",
                    "numberBlocked"
                ]
            },
            "Invoice": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "9001"
                    },
                    "number": {
                        "type": "string",
                        "description": "Serie y correlativo.",
                        "example": "F001-0000123"
                    },
                    "issuedAt": {
                        "type": "string",
                        "example": "2026-10-01"
                    },
                    "dueDate": {
                        "type": "string",
                        "example": "2026-10-15"
                    },
                    "amount": {
                        "type": "number",
                        "description": "Total.",
                        "example": 80
                    },
                    "paid": {
                        "type": "number",
                        "description": "Pagado.",
                        "example": 0
                    },
                    "remaining": {
                        "type": "number",
                        "description": "Pendiente.",
                        "example": 80
                    },
                    "currency": {
                        "type": "string",
                        "example": "PEN"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "partial",
                            "paid",
                            "void"
                        ]
                    },
                    "description": {
                        "type": "string",
                        "example": "Mensualidad octubre",
                        "nullable": true
                    }
                },
                "required": [
                    "id",
                    "number",
                    "amount",
                    "paid",
                    "remaining",
                    "currency",
                    "status"
                ]
            },
            "InvoiceList": {
                "type": "object",
                "properties": {
                    "invoices": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Invoice"
                        }
                    }
                },
                "required": [
                    "invoices"
                ]
            },
            "Ticket": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "3021"
                    },
                    "number": {
                        "type": "string",
                        "example": "T-3021"
                    },
                    "type": {
                        "type": "string",
                        "description": "Motivo del ticket (catálogo de incidencias de la empresa).",
                        "example": "AVERIA INTERNET"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "open",
                            "closed"
                        ]
                    },
                    "createdAt": {
                        "type": "string",
                        "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "example": "2026-10-06 09:12:00"
                    },
                    "summary": {
                        "type": "string",
                        "example": "Sin internet desde la mañana",
                        "nullable": true
                    }
                },
                "required": [
                    "id",
                    "number",
                    "status"
                ]
            },
            "TicketList": {
                "type": "object",
                "properties": {
                    "tickets": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Ticket"
                        },
                        "description": "Hasta 50, el más reciente primero."
                    }
                },
                "required": [
                    "tickets"
                ]
            },
            "TicketCreate": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "outage",
                            "support",
                            "billing",
                            "other"
                        ],
                        "description": "Tipo. Un valor desconocido se toma como other."
                    },
                    "description": {
                        "type": "string",
                        "maxLength": 500,
                        "description": "Obligatoria, máximo 500 caracteres.",
                        "example": "Sin internet desde la mañana"
                    },
                    "notifyCustomer": {
                        "type": "boolean",
                        "description": "Avisar al cliente con la plantilla del sistema (\"ticket registrado\"). Por defecto no."
                    },
                    "notifyMode": {
                        "type": "string",
                        "enum": [
                            "system",
                            "return"
                        ],
                        "description": "Cómo se entrega el aviso: system (por defecto) = este sistema lo envía por sus propios canales de WhatsApp; return = NO lo envía y lo devuelve ya redactado en `notifications` para que lo entregues tú por tu canal."
                    }
                },
                "required": [
                    "description"
                ]
            },
            "TicketCreated": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "3021"
                    },
                    "number": {
                        "type": "string",
                        "example": "T-3021"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "open"
                        ]
                    },
                    "notifications": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Notification"
                        },
                        "description": "Solo con `notifyMode` = `return`: los avisos YA redactados con las plantillas del sistema, para que los entregues por tu canal (el sistema no los envía). Si no hay ninguno, la lista va vacía."
                    }
                },
                "required": [
                    "id",
                    "number",
                    "status"
                ]
            },
            "Notification": {
                "type": "object",
                "properties": {
                    "template": {
                        "type": "string",
                        "description": "Clave de la plantilla del sistema que armó el aviso (Sistema > Plantillas).",
                        "example": "pago_parcial_cliente",
                        "nullable": true
                    },
                    "text": {
                        "type": "string",
                        "description": "El aviso YA redactado, en formato WhatsApp.",
                        "example": "Estimado(a) Ana: hemos recibido tu pago parcial de S/ 40.00."
                    },
                    "variables": {
                        "type": "object",
                        "additionalProperties": {
                            "type": "string"
                        },
                        "description": "Los valores con que se llenó la plantilla (nombre de variable -> valor), por si tu canal usa plantillas propias (por ejemplo las oficiales de Meta).",
                        "example": {
                            "cliente": "Ana Pérez",
                            "monto": "40.00"
                        }
                    },
                    "attachment": {
                        "type": "object",
                        "nullable": true,
                        "description": "El PDF que acompaña al aviso (recibo o comprobante); null si no lleva.",
                        "properties": {
                            "name": {
                                "type": "string",
                                "description": "Nombre visible del archivo.",
                                "example": "Recibo F001-0000123.pdf"
                            },
                            "mimeType": {
                                "type": "string",
                                "example": "application/pdf"
                            },
                            "contentBase64": {
                                "type": "string",
                                "description": "El archivo en base64.",
                                "example": "JVBERi0xLjQK"
                            }
                        }
                    }
                },
                "required": [
                    "text",
                    "variables"
                ]
            },
            "NotificationRequest": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "reminder",
                            "receipt"
                        ],
                        "description": "reminder = recordatorio de la factura pendiente más antigua; receipt = el último comprobante pagado (con su PDF)."
                    }
                },
                "required": [
                    "type"
                ]
            },
            "NotificationResult": {
                "type": "object",
                "properties": {
                    "notifications": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Notification"
                        },
                        "description": "Los avisos ya redactados. NO se envía nada desde este sistema: tú los entregas."
                    }
                },
                "required": [
                    "notifications"
                ]
            },
            "MessageTemplates": {
                "type": "object",
                "properties": {
                    "templates": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "key": {
                                    "type": "string",
                                    "description": "Clave de la plantilla.",
                                    "example": "pago_parcial_cliente"
                                },
                                "title": {
                                    "type": "string",
                                    "description": "Título.",
                                    "example": "PAGO PARCIAL REGISTRADO"
                                },
                                "family": {
                                    "type": "string",
                                    "enum": [
                                        "payments",
                                        "commitments",
                                        "tickets",
                                        "billing",
                                        "installations",
                                        "plans",
                                        "wallet",
                                        "cobros_ia",
                                        "web",
                                        "other"
                                    ],
                                    "description": "Familia del aviso."
                                },
                                "variables": {
                                    "type": "array",
                                    "items": {
                                        "type": "string",
                                        "example": "cliente"
                                    },
                                    "description": "Variables que usa el texto."
                                },
                                "text": {
                                    "type": "string",
                                    "description": "El texto con las variables entre corchetes.",
                                    "example": "Estimado(a) [cliente]: hemos recibido tu pago parcial."
                                }
                            },
                            "required": [
                                "key",
                                "title",
                                "family",
                                "variables",
                                "text"
                            ]
                        },
                        "description": "Las plantillas de aviso al cliente del sistema."
                    }
                },
                "required": [
                    "templates"
                ]
            },
            "PaymentAccount": {
                "type": "object",
                "properties": {
                    "bank": {
                        "type": "string",
                        "description": "Banco o billetera, con el nombre que le da la empresa.",
                        "example": "Yape"
                    },
                    "holder": {
                        "type": "string",
                        "description": "Titular.",
                        "example": "Mi Empresa SAC"
                    },
                    "number": {
                        "type": "string",
                        "description": "Número de cuenta.",
                        "example": "194-1234567-0-12"
                    },
                    "cci": {
                        "type": "string",
                        "description": "Código interbancario (CCI, CLABE, CBU... según el país).",
                        "example": "002-194-001234567012-34",
                        "nullable": true
                    },
                    "phone": {
                        "type": "string",
                        "description": "Celular de la billetera, si aplica.",
                        "example": "51987654321",
                        "nullable": true
                    },
                    "currency": {
                        "type": "string",
                        "example": "PEN"
                    },
                    "note": {
                        "type": "string",
                        "nullable": true
                    }
                },
                "required": [
                    "bank",
                    "holder",
                    "number",
                    "currency"
                ]
            },
            "PaymentMethods": {
                "type": "object",
                "properties": {
                    "accounts": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/PaymentAccount"
                        },
                        "description": "Cuentas de cobro activas del país de la empresa."
                    },
                    "instructions": {
                        "type": "string",
                        "description": "Texto sugerido para el cliente."
                    }
                },
                "required": [
                    "accounts",
                    "instructions"
                ]
            },
            "PromiseCreate": {
                "type": "object",
                "properties": {
                    "promisedDate": {
                        "type": "string",
                        "format": "date",
                        "description": "Fecha prometida (AAAA-MM-DD). Obligatoria.",
                        "example": "2026-10-20"
                    },
                    "note": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "Nota opcional.",
                        "example": "Cobra el viernes"
                    },
                    "invoiceId": {
                        "type": "string",
                        "description": "Factura a la que se refiere (debe ser de este cliente). Opcional.",
                        "example": "9001"
                    },
                    "promisedTime": {
                        "type": "string",
                        "description": "Hora límite (HH:MM, 24 horas). Opcional.",
                        "example": "18:00"
                    },
                    "amount": {
                        "type": "number",
                        "description": "Monto comprometido. Opcional: con `invoiceId` y sin monto, el pendiente de esa factura.",
                        "example": 80
                    },
                    "reminderDate": {
                        "type": "string",
                        "description": "Fecha del recordatorio (AAAA-MM-DD): antes o el mismo día de `promisedDate`. Opcional.",
                        "example": "2026-10-19"
                    },
                    "reminderTime": {
                        "type": "string",
                        "description": "Hora del recordatorio (HH:MM, 24 horas). Opcional; solo con `reminderDate`.",
                        "example": "09:00"
                    },
                    "remind": {
                        "type": "boolean",
                        "description": "El sistema le recuerda al cliente en la fecha del recordatorio (con la plantilla del compromiso). Por defecto no."
                    },
                    "notifyNow": {
                        "type": "boolean",
                        "description": "Avisar YA al cliente de que su compromiso quedó registrado (plantilla del sistema). Por defecto no."
                    },
                    "notifyMode": {
                        "type": "string",
                        "enum": [
                            "system",
                            "return"
                        ],
                        "description": "Cómo se entrega el aviso: system (por defecto) = este sistema lo envía por sus propios canales de WhatsApp; return = NO lo envía y lo devuelve ya redactado en `notifications` para que lo entregues tú por tu canal."
                    }
                },
                "required": [
                    "promisedDate"
                ]
            },
            "PromiseCreated": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "Id del compromiso vigente.",
                        "example": "55",
                        "nullable": true
                    },
                    "promisedDate": {
                        "type": "string",
                        "example": "2026-10-20"
                    },
                    "serviceStatus": {
                        "type": "string",
                        "enum": [
                            "active",
                            "suspended",
                            "pending",
                            "retired"
                        ],
                        "description": "Estado del servicio tras registrar la promesa (puede reactivarse)."
                    },
                    "notifications": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Notification"
                        },
                        "description": "Solo con `notifyMode` = `return`: los avisos YA redactados con las plantillas del sistema, para que los entregues por tu canal (el sistema no los envía). Si no hay ninguno, la lista va vacía."
                    }
                },
                "required": [
                    "promisedDate",
                    "serviceStatus"
                ]
            },
            "ProofCreate": {
                "type": "object",
                "properties": {
                    "fileUrl": {
                        "type": "string",
                        "description": "Dirección https de la imagen o PDF del comprobante. Obligatoria.",
                        "example": "https://ejemplo.com/comprobante.jpg"
                    },
                    "mimeType": {
                        "type": "string",
                        "description": "Tipo del archivo. Por defecto image/jpeg.",
                        "example": "image/jpeg"
                    },
                    "text": {
                        "type": "string",
                        "description": "Texto que acompañó al comprobante."
                    }
                },
                "required": [
                    "fileUrl"
                ]
            },
            "ProofReceived": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "Identificador de la captura.",
                        "example": "cap-1a2b3c4d5e6f"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "received"
                        ]
                    }
                },
                "required": [
                    "id",
                    "status"
                ],
                "description": "Se responde 202 al instante: la lectura y la conciliación siguen en segundo plano."
            },
            "ServiceStatus": {
                "type": "object",
                "properties": {
                    "status": {
                        "type": "string",
                        "enum": [
                            "active",
                            "suspended",
                            "pending",
                            "retired"
                        ]
                    },
                    "suspendedSince": {
                        "type": "string",
                        "nullable": true
                    },
                    "plan": {
                        "type": "string",
                        "example": "Fibra 100",
                        "nullable": true
                    },
                    "speed": {
                        "description": "Velocidad contratada (número).",
                        "example": 100
                    },
                    "speedUnit": {
                        "type": "string",
                        "example": "Mbps",
                        "nullable": true
                    },
                    "technology": {
                        "type": "string",
                        "example": "Fibra",
                        "nullable": true
                    },
                    "router": {
                        "type": "string",
                        "example": "RB-Norte",
                        "nullable": true
                    },
                    "connection": {
                        "type": "object",
                        "properties": {
                            "state": {
                                "type": "string",
                                "enum": [
                                    "online",
                                    "offline",
                                    "unknown"
                                ]
                            },
                            "lastSeen": {
                                "type": "string",
                                "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                "nullable": true
                            }
                        },
                        "required": [
                            "state"
                        ]
                    },
                    "onu": {
                        "type": "object",
                        "nullable": true,
                        "description": "null si el cliente no tiene ONU vinculada.",
                        "properties": {
                            "serial": {
                                "type": "string",
                                "example": "TPLG12345678",
                                "nullable": true
                            },
                            "olt": {
                                "type": "string",
                                "example": "OLT-1",
                                "nullable": true
                            },
                            "pon": {
                                "description": "Puerto PON.",
                                "example": "1/1/3"
                            },
                            "state": {
                                "type": "string",
                                "enum": [
                                    "online",
                                    "offline",
                                    "unknown"
                                ]
                            },
                            "cause": {
                                "type": "string",
                                "description": "Causa de la caída, si el monitor la conoce.",
                                "nullable": true
                            },
                            "rxDbm": {
                                "type": "number",
                                "description": "Potencia óptica recibida (dBm).",
                                "example": -21.4,
                                "nullable": true
                            },
                            "rxLevel": {
                                "type": "string",
                                "enum": [
                                    "good",
                                    "weak",
                                    "critical"
                                ],
                                "description": "good ≥ -25 dBm; weak hasta -27.5; critical por debajo.",
                                "nullable": true
                            },
                            "updatedAt": {
                                "type": "string",
                                "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                "example": "2026-10-06 10:40:00",
                                "nullable": true
                            }
                        }
                    },
                    "actions": {
                        "type": "object",
                        "properties": {
                            "canSuspend": {
                                "type": "boolean"
                            },
                            "canActivate": {
                                "type": "boolean"
                            }
                        },
                        "required": [
                            "canSuspend",
                            "canActivate"
                        ]
                    }
                },
                "required": [
                    "status",
                    "connection",
                    "actions"
                ],
                "description": "Lo deja el monitor de la OLT y el colector de tráfico: no consulta el equipo en vivo."
            },
            "ServiceChange": {
                "type": "object",
                "properties": {
                    "status": {
                        "type": "string",
                        "enum": [
                            "active",
                            "suspended"
                        ]
                    },
                    "changed": {
                        "type": "boolean",
                        "description": "false si el servicio ya estaba en ese estado (repetir no hace nada)."
                    },
                    "network": {
                        "type": "object",
                        "properties": {
                            "applied": {
                                "type": "boolean",
                                "description": "El corte o la reconexión se aplicó en el router."
                            },
                            "note": {
                                "type": "string",
                                "nullable": true
                            }
                        },
                        "required": [
                            "applied"
                        ]
                    }
                },
                "required": [
                    "status",
                    "changed"
                ]
            },
            "TrafficPoint": {
                "type": "object",
                "properties": {
                    "at": {
                        "type": "string",
                        "description": "Hora (AAAA-MM-DD HH:00) o día (AAAA-MM-DD), en la zona de la empresa.",
                        "example": "2026-10-06 09:00"
                    },
                    "downloadBytes": {
                        "type": "integer",
                        "example": 120000000
                    },
                    "uploadBytes": {
                        "type": "integer",
                        "example": 8000000
                    }
                },
                "required": [
                    "at",
                    "downloadBytes",
                    "uploadBytes"
                ]
            },
            "Traffic": {
                "type": "object",
                "properties": {
                    "range": {
                        "type": "string",
                        "enum": [
                            "24h",
                            "7d",
                            "30d"
                        ]
                    },
                    "unit": {
                        "type": "string",
                        "enum": [
                            "hour",
                            "day"
                        ],
                        "description": "hour para 24h; day para 7d y 30d."
                    },
                    "monitored": {
                        "type": "boolean",
                        "description": "false si el router del cliente no envía datos de consumo."
                    },
                    "lastTrafficAt": {
                        "type": "string",
                        "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "nullable": true
                    },
                    "totals": {
                        "type": "object",
                        "properties": {
                            "downloadBytes": {
                                "type": "integer"
                            },
                            "uploadBytes": {
                                "type": "integer"
                            }
                        },
                        "required": [
                            "downloadBytes",
                            "uploadBytes"
                        ]
                    },
                    "peak": {
                        "type": "object",
                        "nullable": true,
                        "description": "Pico de descarga.",
                        "properties": {
                            "at": {
                                "type": "string"
                            },
                            "downloadBytes": {
                                "type": "integer"
                            }
                        }
                    },
                    "points": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/TrafficPoint"
                        }
                    }
                },
                "required": [
                    "range",
                    "unit",
                    "monitored",
                    "totals",
                    "points"
                ],
                "description": "downloadBytes es la descarga del cliente; uploadBytes, su subida."
            },
            "CustomerPage": {
                "type": "object",
                "properties": {
                    "customers": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Customer"
                        }
                    },
                    "nextCursor": {
                        "type": "string",
                        "description": "Pásalo como `after` para pedir la página siguiente. null = no hay más.",
                        "example": "1234",
                        "nullable": true
                    }
                },
                "required": [
                    "customers",
                    "nextCursor"
                ]
            },
            "InvoiceLine": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": [
                            "service",
                            "product",
                            "free"
                        ],
                        "description": "Servicio, producto o línea libre."
                    },
                    "description": {
                        "type": "string",
                        "example": "Servicio de internet, mes de octubre"
                    },
                    "quantity": {
                        "type": "integer",
                        "example": 1
                    },
                    "price": {
                        "type": "number",
                        "example": 80
                    },
                    "taxPercent": {
                        "type": "number",
                        "example": 0
                    },
                    "total": {
                        "type": "number",
                        "example": 80
                    }
                },
                "required": [
                    "type",
                    "description",
                    "quantity",
                    "price",
                    "total"
                ]
            },
            "Payment": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "5501"
                    },
                    "invoiceId": {
                        "type": "string",
                        "description": "Factura a la que se aplicó.",
                        "example": "9001",
                        "nullable": true
                    },
                    "invoiceNumber": {
                        "type": "string",
                        "example": "F001-0000123",
                        "nullable": true
                    },
                    "date": {
                        "type": "string",
                        "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "example": "2026-10-05 14:30:00"
                    },
                    "amount": {
                        "type": "number",
                        "description": "Monto pagado.",
                        "example": 80
                    },
                    "currency": {
                        "type": "string",
                        "example": "PEN"
                    },
                    "method": {
                        "type": "string",
                        "description": "Forma de pago, con el nombre que le da la empresa.",
                        "example": "YAPE",
                        "nullable": true
                    },
                    "reference": {
                        "type": "string",
                        "description": "Número de operación.",
                        "example": "00845122",
                        "nullable": true
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "valid",
                            "void"
                        ],
                        "description": "void = pago anulado (no se borra)."
                    }
                },
                "required": [
                    "id",
                    "date",
                    "amount",
                    "currency",
                    "status"
                ]
            },
            "PaymentWithCustomer": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Payment"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "customer": {
                                "type": "object",
                                "properties": {
                                    "id": {
                                        "type": "string",
                                        "example": "1234"
                                    },
                                    "name": {
                                        "type": "string",
                                        "example": "Ana Pérez"
                                    }
                                },
                                "required": [
                                    "id",
                                    "name"
                                ]
                            }
                        },
                        "required": [
                            "customer"
                        ]
                    }
                ]
            },
            "PaymentList": {
                "type": "object",
                "properties": {
                    "payments": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Payment"
                        }
                    },
                    "nextCursor": {
                        "type": "string",
                        "description": "Pásalo como `after` para la página siguiente. null = no hay más.",
                        "nullable": true
                    }
                },
                "required": [
                    "payments",
                    "nextCursor"
                ]
            },
            "PaymentReport": {
                "type": "object",
                "properties": {
                    "payments": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/PaymentWithCustomer"
                        }
                    },
                    "nextCursor": {
                        "type": "string",
                        "description": "Pásalo como `after` para la página siguiente. null = no hay más.",
                        "nullable": true
                    }
                },
                "required": [
                    "payments",
                    "nextCursor"
                ]
            },
            "InvoiceDetail": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Invoice"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "period": {
                                "type": "object",
                                "nullable": true,
                                "description": "Periodo que cubre (facturas de servicio).",
                                "properties": {
                                    "from": {
                                        "type": "string",
                                        "example": "2026-10-01"
                                    },
                                    "to": {
                                        "type": "string",
                                        "example": "2026-10-31"
                                    }
                                }
                            },
                            "subtotal": {
                                "type": "number",
                                "example": 80
                            },
                            "discount": {
                                "type": "number",
                                "example": 0
                            },
                            "tax": {
                                "type": "object",
                                "properties": {
                                    "amount": {
                                        "type": "number",
                                        "example": 0
                                    },
                                    "percent": {
                                        "type": "number",
                                        "example": 0
                                    },
                                    "included": {
                                        "type": "boolean",
                                        "description": "El precio ya incluye el impuesto."
                                    }
                                },
                                "required": [
                                    "amount",
                                    "percent",
                                    "included"
                                ]
                            },
                            "installments": {
                                "type": "object",
                                "nullable": true,
                                "description": "Solo si la factura se financió en cuotas.",
                                "properties": {
                                    "total": {
                                        "type": "integer",
                                        "example": 3
                                    },
                                    "interestPercent": {
                                        "type": "number",
                                        "example": 0,
                                        "nullable": true
                                    },
                                    "interestAmount": {
                                        "type": "number",
                                        "example": 0,
                                        "nullable": true
                                    }
                                }
                            },
                            "lines": {
                                "type": "array",
                                "items": {
                                    "$ref": "#/components/schemas/InvoiceLine"
                                }
                            },
                            "payments": {
                                "type": "array",
                                "items": {
                                    "$ref": "#/components/schemas/Payment"
                                },
                                "description": "Pagos aplicados a esta factura, del más antiguo al más nuevo."
                            }
                        },
                        "required": [
                            "subtotal",
                            "discount",
                            "tax",
                            "lines",
                            "payments"
                        ]
                    }
                ]
            },
            "TicketDetail": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "3021"
                    },
                    "number": {
                        "type": "string",
                        "example": "T-3021"
                    },
                    "type": {
                        "type": "string",
                        "description": "Motivo del ticket.",
                        "example": "AVERIA INTERNET"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "open",
                            "closed"
                        ]
                    },
                    "stage": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "in_progress",
                            "resolved",
                            "cancelled"
                        ],
                        "description": "Etapa: pendiente, en proceso, resuelto o cancelado."
                    },
                    "priority": {
                        "type": "string",
                        "enum": [
                            "low",
                            "medium",
                            "high",
                            "urgent"
                        ]
                    },
                    "description": {
                        "type": "string",
                        "example": "Sin internet desde la mañana",
                        "nullable": true
                    },
                    "scheduledAt": {
                        "type": "string",
                        "description": "Fecha programada de atención. Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "example": "2026-10-06 09:12:00"
                    },
                    "openedAt": {
                        "type": "string",
                        "description": "Cuando el técnico empezó. Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "example": "2026-10-06 10:00:00",
                        "nullable": true
                    },
                    "closedAt": {
                        "type": "string",
                        "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "nullable": true
                    },
                    "createdAt": {
                        "type": "string",
                        "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "example": "2026-10-06 09:12:00"
                    },
                    "technician": {
                        "type": "string",
                        "description": "Técnico asignado.",
                        "example": "Luis Rojas",
                        "nullable": true
                    },
                    "resolution": {
                        "type": "object",
                        "nullable": true,
                        "description": "La solución que registró el técnico al cerrar.",
                        "properties": {
                            "comment": {
                                "type": "string",
                                "example": "Se cambió el conector",
                                "nullable": true
                            },
                            "closedAt": {
                                "type": "string",
                                "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                "example": "2026-10-06 11:30:00"
                            }
                        }
                    }
                },
                "required": [
                    "id",
                    "number",
                    "type",
                    "status",
                    "stage",
                    "priority",
                    "scheduledAt",
                    "createdAt"
                ]
            },
            "NetworkService": {
                "type": "object",
                "properties": {
                    "plan": {
                        "type": "string",
                        "example": "Fibra 100"
                    },
                    "speed": {
                        "description": "Velocidad contratada (número).",
                        "example": 100
                    },
                    "speedUnit": {
                        "type": "string",
                        "example": "MBPS",
                        "nullable": true
                    },
                    "technology": {
                        "type": "string",
                        "example": "FIBRA",
                        "nullable": true
                    },
                    "connectionType": {
                        "type": "string",
                        "enum": [
                            "internet",
                            "personalizado"
                        ],
                        "description": "personalizado = servicio a medida, sin conexión de internet gestionada."
                    },
                    "router": {
                        "type": "object",
                        "nullable": true,
                        "properties": {
                            "id": {
                                "type": "string",
                                "example": "1"
                            },
                            "name": {
                                "type": "string",
                                "example": "RB-Norte"
                            }
                        }
                    },
                    "pppoe": {
                        "type": "object",
                        "nullable": true,
                        "description": "Usuario y perfil PPPoE. La clave nunca se entrega.",
                        "properties": {
                            "user": {
                                "type": "string",
                                "example": "72194956"
                            },
                            "profile": {
                                "type": "string",
                                "example": "100Mbps/100Mbps",
                                "nullable": true
                            }
                        }
                    },
                    "ipAddress": {
                        "type": "string",
                        "description": "IP asignada.",
                        "example": "10.10.0.15",
                        "nullable": true
                    },
                    "speedLimit": {
                        "type": "string",
                        "nullable": true
                    },
                    "queue": {
                        "type": "string",
                        "description": "Cola simple del router.",
                        "nullable": true
                    }
                },
                "required": [
                    "plan",
                    "connectionType"
                ]
            },
            "CustomerNetwork": {
                "type": "object",
                "properties": {
                    "services": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/NetworkService"
                        },
                        "description": "Servicios activos del cliente."
                    }
                },
                "required": [
                    "services"
                ]
            },
            "Router": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "1"
                    },
                    "name": {
                        "type": "string",
                        "example": "RB-Norte"
                    },
                    "model": {
                        "type": "string",
                        "example": "RB4011",
                        "nullable": true
                    },
                    "routerosVersion": {
                        "type": "string",
                        "example": "7.14",
                        "nullable": true
                    },
                    "managed": {
                        "type": "boolean",
                        "description": "El sistema está conectado a este router para gestionarlo."
                    },
                    "apiEnabled": {
                        "type": "boolean",
                        "description": "false = el sistema lo trata como desconectado (pruebas o emergencias)."
                    },
                    "customers": {
                        "type": "integer",
                        "description": "Clientes con un servicio activo en este router.",
                        "example": 120
                    },
                    "trafficMonitoring": {
                        "type": "object",
                        "properties": {
                            "enabled": {
                                "type": "boolean"
                            },
                            "status": {
                                "type": "string",
                                "example": "activo"
                            },
                            "lastSeen": {
                                "type": "string",
                                "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                "nullable": true
                            }
                        },
                        "required": [
                            "enabled",
                            "status"
                        ]
                    }
                },
                "required": [
                    "id",
                    "name",
                    "managed",
                    "apiEnabled",
                    "customers",
                    "trafficMonitoring"
                ],
                "description": "Nunca se entregan su dirección de gestión, usuarios ni claves."
            },
            "RouterList": {
                "type": "object",
                "properties": {
                    "routers": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Router"
                        }
                    }
                },
                "required": [
                    "routers"
                ]
            },
            "CustomerOnu": {
                "type": "object",
                "properties": {
                    "onu": {
                        "type": "object",
                        "nullable": true,
                        "description": "null si el cliente no tiene ONU vinculada.",
                        "properties": {
                            "serial": {
                                "type": "string",
                                "example": "TPLG12345678"
                            },
                            "olt": {
                                "type": "string",
                                "example": "OLT-1",
                                "nullable": true
                            },
                            "pon": {
                                "description": "Puerto PON.",
                                "example": "1/1/3"
                            },
                            "state": {
                                "type": "string",
                                "enum": [
                                    "online",
                                    "offline",
                                    "unknown"
                                ]
                            },
                            "cause": {
                                "type": "string",
                                "description": "Causa de la caída, si el monitor la conoce.",
                                "nullable": true
                            },
                            "rxDbm": {
                                "type": "number",
                                "description": "Potencia óptica recibida (dBm).",
                                "example": -21.4,
                                "nullable": true
                            },
                            "rxLevel": {
                                "type": "string",
                                "enum": [
                                    "good",
                                    "weak",
                                    "critical"
                                ],
                                "description": "good ≥ -25 dBm; weak hasta -27.5; critical por debajo.",
                                "nullable": true
                            },
                            "updatedAt": {
                                "type": "string",
                                "description": "Último estado del monitor. Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                "example": "2026-10-06 10:40:00",
                                "nullable": true
                            },
                            "linkedAt": {
                                "type": "string",
                                "description": "Cuándo se vinculó. Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                "example": "2026-03-02 09:00:00",
                                "nullable": true
                            }
                        }
                    }
                },
                "required": [
                    "onu"
                ],
                "description": "Lo deja el monitor de la OLT: no consulta el equipo en vivo."
            },
            "Olt": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "1"
                    },
                    "name": {
                        "type": "string",
                        "example": "OLT-1"
                    },
                    "vendor": {
                        "type": "string",
                        "example": "HUAWEI",
                        "nullable": true
                    },
                    "model": {
                        "type": "string",
                        "example": "MA5800",
                        "nullable": true
                    },
                    "zone": {
                        "type": "string",
                        "example": "Zona Norte",
                        "nullable": true
                    },
                    "onus": {
                        "type": "object",
                        "properties": {
                            "linked": {
                                "type": "integer",
                                "description": "ONU vinculadas a clientes.",
                                "example": 120
                            },
                            "online": {
                                "type": "integer",
                                "example": 110
                            },
                            "offline": {
                                "type": "integer",
                                "example": 8
                            },
                            "unknown": {
                                "type": "integer",
                                "description": "Sin estado todavía.",
                                "example": 2
                            }
                        },
                        "required": [
                            "linked",
                            "online",
                            "offline",
                            "unknown"
                        ]
                    },
                    "lastUpdate": {
                        "type": "string",
                        "description": "Última lectura del monitor. Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "example": "2026-10-06 10:40:00",
                        "nullable": true
                    }
                },
                "required": [
                    "id",
                    "name",
                    "onus"
                ],
                "description": "Nunca se entregan sus conexiones (dirección, usuario ni clave)."
            },
            "OltList": {
                "type": "object",
                "properties": {
                    "olts": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Olt"
                        }
                    }
                },
                "required": [
                    "olts"
                ]
            },
            "TicketClose": {
                "type": "object",
                "properties": {
                    "resolution": {
                        "type": "string",
                        "maxLength": 500,
                        "description": "La solución que se registra al cerrar. Obligatoria, máximo 500 caracteres.",
                        "example": "Se cambió el conector"
                    },
                    "notifyCustomer": {
                        "type": "boolean",
                        "description": "Avisar al cliente que su ticket se resolvió (plantilla del sistema, con el PDF de la solución). Por defecto no."
                    },
                    "notifyMode": {
                        "type": "string",
                        "enum": [
                            "system",
                            "return"
                        ],
                        "description": "Cómo se entrega el aviso: system (por defecto) = este sistema lo envía por sus propios canales de WhatsApp; return = NO lo envía y lo devuelve ya redactado en `notifications` para que lo entregues tú por tu canal."
                    }
                },
                "required": [
                    "resolution"
                ]
            },
            "TicketClosed": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "example": "3021"
                    },
                    "number": {
                        "type": "string",
                        "example": "T-3021"
                    },
                    "status": {
                        "type": "string",
                        "enum": [
                            "open",
                            "closed"
                        ]
                    },
                    "stage": {
                        "type": "string",
                        "enum": [
                            "pending",
                            "in_progress",
                            "resolved",
                            "cancelled"
                        ]
                    },
                    "changed": {
                        "type": "boolean",
                        "description": "false si el ticket ya estaba resuelto (repetir no hace nada)."
                    },
                    "closedAt": {
                        "type": "string",
                        "description": "Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                        "example": "2026-10-06 11:30:00",
                        "nullable": true
                    },
                    "notifications": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Notification"
                        },
                        "description": "Solo con `notifyMode` = `return`: los avisos YA redactados con las plantillas del sistema, para que los entregues por tu canal (el sistema no los envía). Si no hay ninguno, la lista va vacía."
                    }
                },
                "required": [
                    "id",
                    "number",
                    "status",
                    "stage",
                    "changed"
                ]
            },
            "PaymentOptions": {
                "type": "object",
                "properties": {
                    "currency": {
                        "type": "string",
                        "description": "Moneda de la empresa (ISO 4217).",
                        "example": "PEN"
                    },
                    "methods": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "id": {
                                    "type": "string",
                                    "description": "Id de la forma de pago (úsalo en `methodId`).",
                                    "example": "1"
                                },
                                "name": {
                                    "type": "string",
                                    "example": "Efectivo"
                                }
                            },
                            "required": [
                                "id",
                                "name"
                            ]
                        },
                        "description": "Formas de pago activas de la empresa."
                    },
                    "canRegister": {
                        "type": "boolean",
                        "description": "El perfil del usuario de la llave puede registrar cobros."
                    },
                    "commitment": {
                        "type": "object",
                        "nullable": true,
                        "description": "Compromiso de pago abierto del cliente; null si no tiene.",
                        "properties": {
                            "deadline": {
                                "type": "string",
                                "description": "Fecha límite (AAAA-MM-DD).",
                                "example": "2026-10-20"
                            },
                            "deadlineTime": {
                                "type": "string",
                                "description": "Hora límite (HH:MM).",
                                "example": "18:00",
                                "nullable": true
                            },
                            "amount": {
                                "type": "number",
                                "description": "Monto comprometido.",
                                "example": 80
                            }
                        }
                    },
                    "commonAmounts": {
                        "type": "array",
                        "items": {
                            "type": "number",
                            "example": 80
                        },
                        "description": "Montos de factura de servicio más frecuentes de la empresa (atajos para el cobro)."
                    },
                    "invoices": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "id": {
                                    "type": "string",
                                    "example": "9001"
                                },
                                "number": {
                                    "type": "string",
                                    "description": "Serie y correlativo.",
                                    "example": "F001-0000123"
                                },
                                "period": {
                                    "type": "string",
                                    "description": "Mes o periodo de la factura.",
                                    "example": "octubre 2026",
                                    "nullable": true
                                },
                                "description": {
                                    "type": "string",
                                    "description": "Producto, en ventas libres.",
                                    "nullable": true
                                },
                                "kind": {
                                    "type": "string",
                                    "enum": [
                                        "service",
                                        "sale"
                                    ],
                                    "description": "Factura de servicio o venta libre."
                                },
                                "amount": {
                                    "type": "number",
                                    "description": "Total.",
                                    "example": 80
                                },
                                "remaining": {
                                    "type": "number",
                                    "description": "Pendiente.",
                                    "example": 80
                                },
                                "suggested": {
                                    "type": "number",
                                    "description": "Monto sugerido a cobrar: lo pendiente, o la cuota que toca en una venta a cuotas.",
                                    "example": 80
                                },
                                "installment": {
                                    "type": "string",
                                    "description": "Cuota que toca (ej. 2/6) en una venta a cuotas.",
                                    "nullable": true
                                },
                                "issuedAt": {
                                    "type": "string",
                                    "example": "2026-10-01"
                                },
                                "dueDate": {
                                    "type": "string",
                                    "example": "2026-10-15"
                                },
                                "status": {
                                    "type": "string",
                                    "enum": [
                                        "pending",
                                        "partial",
                                        "paid",
                                        "void"
                                    ]
                                }
                            },
                            "required": [
                                "id",
                                "number",
                                "kind",
                                "amount",
                                "remaining",
                                "suggested",
                                "issuedAt",
                                "dueDate",
                                "status"
                            ]
                        },
                        "description": "Facturas pendientes de cobro, la más reciente primero."
                    }
                },
                "required": [
                    "currency",
                    "methods",
                    "canRegister",
                    "commitment",
                    "commonAmounts",
                    "invoices"
                ]
            },
            "PaymentCreate": {
                "type": "object",
                "properties": {
                    "invoiceIds": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "example": "9001"
                        },
                        "description": "Facturas a cobrar (de 1 a 50): deben ser de este cliente y estar pendientes. El monto se reparte en cascada entre ellas."
                    },
                    "amount": {
                        "type": "number",
                        "description": "Monto a cobrar. Puede ser menor que lo pendiente (pago parcial); no puede pasar de lo que se debe en esas facturas.",
                        "example": 80
                    },
                    "methodId": {
                        "type": "string",
                        "description": "Forma de pago (de `GET .../payments/options`).",
                        "example": "1"
                    },
                    "operation": {
                        "type": "string",
                        "maxLength": 60,
                        "description": "Número de operación o referencia. Opcional.",
                        "example": "00123456"
                    },
                    "comment": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "Comentario. Opcional.",
                        "example": "Pagó por Yape"
                    },
                    "activateService": {
                        "type": "boolean",
                        "description": "Reactivar el servicio si el pago lo permite (quita el corte). Por defecto sí."
                    },
                    "notifyCustomer": {
                        "type": "boolean",
                        "description": "Avisar al cliente con la plantilla que corresponda (pago parcial, completo, varias facturas, cuotas...) y su PDF. Por defecto no."
                    },
                    "notifyMode": {
                        "type": "string",
                        "enum": [
                            "system",
                            "return"
                        ],
                        "description": "Cómo se entrega el aviso: system (por defecto) = este sistema lo envía por sus propios canales de WhatsApp; return = NO lo envía y lo devuelve ya redactado en `notifications` para que lo entregues tú por tu canal."
                    },
                    "balance": {
                        "type": "object",
                        "properties": {
                            "deadline": {
                                "type": "string",
                                "description": "Plazo para el saldo (AAAA-MM-DD), si el pago es parcial.",
                                "example": "2026-10-25"
                            },
                            "deadlineTime": {
                                "type": "string",
                                "description": "Hora del plazo (HH:MM).",
                                "example": "18:00"
                            },
                            "reminderDate": {
                                "type": "string",
                                "description": "Recordatorio del saldo (AAAA-MM-DD): antes o el mismo día del plazo.",
                                "example": "2026-10-24"
                            },
                            "reminderTime": {
                                "type": "string",
                                "description": "Hora del recordatorio (HH:MM).",
                                "example": "09:00"
                            }
                        },
                        "description": "Plazo y recordatorio del saldo en un pago parcial. Todo opcional."
                    }
                },
                "required": [
                    "invoiceIds",
                    "amount",
                    "methodId"
                ]
            },
            "PaymentCreated": {
                "type": "object",
                "properties": {
                    "registered": {
                        "type": "boolean",
                        "description": "Siempre true: el cobro quedó registrado."
                    },
                    "amount": {
                        "type": "number",
                        "description": "Monto cobrado.",
                        "example": 40
                    },
                    "invoiceIds": {
                        "type": "array",
                        "items": {
                            "type": "string",
                            "example": "9001"
                        },
                        "description": "Facturas sobre las que se cobró."
                    },
                    "message": {
                        "type": "string",
                        "description": "Resultado que informa el sistema (compromiso, servicio reactivado, aviso...).",
                        "example": "Se ha actualizado el registro exitosamente."
                    },
                    "balance": {
                        "type": "object",
                        "properties": {
                            "amount": {
                                "type": "number",
                                "description": "Lo que debe ahora.",
                                "example": 40
                            },
                            "currency": {
                                "type": "string",
                                "example": "PEN"
                            },
                            "overdueInvoices": {
                                "type": "integer",
                                "description": "Facturas vencidas.",
                                "example": 0
                            }
                        },
                        "required": [
                            "amount",
                            "currency"
                        ]
                    },
                    "serviceStatus": {
                        "type": "string",
                        "enum": [
                            "active",
                            "suspended",
                            "pending",
                            "retired"
                        ],
                        "description": "Estado del servicio tras el cobro."
                    },
                    "notifications": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Notification"
                        },
                        "description": "Solo con `notifyMode` = `return`: los avisos YA redactados con las plantillas del sistema, para que los entregues por tu canal (el sistema no los envía). Si no hay ninguno, la lista va vacía."
                    }
                },
                "required": [
                    "registered",
                    "amount",
                    "invoiceIds",
                    "message"
                ]
            },
            "WebhookEvent": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "Identificador del evento: el mismo no se entrega dos veces como evento distinto. Úsalo para no procesar un evento repetido.",
                        "example": "evt_9f2c4a1b7d3e5f60a1b2c3d4"
                    },
                    "type": {
                        "type": "string",
                        "description": "Tipo del evento.",
                        "example": "payment.registered"
                    },
                    "apiVersion": {
                        "type": "string",
                        "example": "v1"
                    },
                    "createdAt": {
                        "type": "string",
                        "description": "Cuándo se detectó (ISO 8601, con la zona horaria de la empresa).",
                        "example": "2026-10-06T14:30:05-05:00"
                    },
                    "data": {
                        "type": "object",
                        "description": "Lo propio de cada evento (ver la lista de eventos)."
                    }
                },
                "required": [
                    "id",
                    "type",
                    "apiVersion",
                    "createdAt",
                    "data"
                ]
            },
            "WebhookInvoice": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/Invoice"
                    },
                    {
                        "type": "object",
                        "properties": {
                            "customer": {
                                "type": "object",
                                "properties": {
                                    "id": {
                                        "type": "string",
                                        "example": "1234"
                                    },
                                    "name": {
                                        "type": "string",
                                        "example": "Ana Pérez"
                                    }
                                },
                                "required": [
                                    "id",
                                    "name"
                                ]
                            }
                        },
                        "required": [
                            "customer"
                        ]
                    }
                ]
            },
            "WebhookTicket": {
                "type": "object",
                "properties": {
                    "ticket": {
                        "allOf": [
                            {
                                "$ref": "#/components/schemas/TicketDetail"
                            },
                            {
                                "type": "object",
                                "properties": {
                                    "customer": {
                                        "type": "object",
                                        "properties": {
                                            "id": {
                                                "type": "string",
                                                "example": "1234"
                                            },
                                            "name": {
                                                "type": "string",
                                                "example": "Ana Pérez"
                                            }
                                        },
                                        "required": [
                                            "id",
                                            "name"
                                        ]
                                    }
                                },
                                "required": [
                                    "customer"
                                ]
                            }
                        ]
                    }
                },
                "required": [
                    "ticket"
                ]
            },
            "WebhookService": {
                "type": "object",
                "properties": {
                    "customer": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string",
                                "example": "1234"
                            },
                            "name": {
                                "type": "string",
                                "example": "Ana Pérez"
                            }
                        },
                        "required": [
                            "id",
                            "name"
                        ]
                    },
                    "service": {
                        "type": "object",
                        "properties": {
                            "status": {
                                "type": "string",
                                "enum": [
                                    "active",
                                    "suspended",
                                    "pending",
                                    "retired"
                                ],
                                "description": "Estado nuevo."
                            },
                            "previousStatus": {
                                "type": "string",
                                "enum": [
                                    "active",
                                    "suspended",
                                    "pending",
                                    "retired"
                                ],
                                "description": "Estado anterior."
                            }
                        },
                        "required": [
                            "status",
                            "previousStatus"
                        ]
                    }
                },
                "required": [
                    "customer",
                    "service"
                ]
            },
            "WebhookCustomer": {
                "type": "object",
                "properties": {
                    "customer": {
                        "$ref": "#/components/schemas/Customer"
                    }
                },
                "required": [
                    "customer"
                ]
            },
            "WebhookOnu": {
                "type": "object",
                "properties": {
                    "onu": {
                        "type": "object",
                        "properties": {
                            "serial": {
                                "type": "string",
                                "example": "TPLG12345678"
                            },
                            "olt": {
                                "type": "string",
                                "example": "OLT-1",
                                "nullable": true
                            },
                            "pon": {
                                "description": "Puerto PON.",
                                "example": "1/1/3"
                            },
                            "state": {
                                "type": "string",
                                "enum": [
                                    "offline"
                                ]
                            },
                            "cause": {
                                "type": "string",
                                "description": "Causa de la caída que registró el monitor.",
                                "example": "LOS",
                                "nullable": true
                            },
                            "rxDbm": {
                                "type": "number",
                                "example": -27.9,
                                "nullable": true
                            },
                            "rxLevel": {
                                "type": "string",
                                "enum": [
                                    "good",
                                    "weak",
                                    "critical"
                                ],
                                "nullable": true
                            },
                            "at": {
                                "type": "string",
                                "description": "Cuándo la registró el monitor. Fecha y hora en la zona horaria de la empresa (AAAA-MM-DD HH:MM:SS).",
                                "example": "2026-10-06 14:25:00"
                            }
                        },
                        "required": [
                            "serial",
                            "state",
                            "at"
                        ]
                    },
                    "customer": {
                        "type": "object",
                        "nullable": true,
                        "description": "El cliente al que está vinculada la ONU; null si no está vinculada.",
                        "properties": {
                            "id": {
                                "type": "string",
                                "example": "1234"
                            },
                            "name": {
                                "type": "string",
                                "example": "Ana Pérez"
                            }
                        }
                    }
                },
                "required": [
                    "onu",
                    "customer"
                ]
            },
            "OnuRebootResult": {
                "type": "object",
                "properties": {
                    "requested": {
                        "type": "boolean",
                        "description": "El reinicio se envió a la OLT."
                    },
                    "serial": {
                        "type": "string",
                        "example": "TPLG12345678"
                    },
                    "olt": {
                        "type": "string",
                        "example": "OLT-1",
                        "nullable": true
                    },
                    "pon": {
                        "description": "Puerto PON.",
                        "example": "1/1/3"
                    },
                    "note": {
                        "type": "string",
                        "example": "Reinicio enviado: el cliente pierde la conexión unos minutos."
                    }
                },
                "required": [
                    "requested",
                    "serial"
                ]
            }
        }
    },
    "x-webhook-envelope": {
        "$ref": "#/components/schemas/WebhookEvent"
    },
    "x-webhooks": [
        {
            "type": "payment.registered",
            "label": "Pago registrado",
            "description": "Se registró un pago (por cualquier vía: caja, comprobante leído por Cobros IA, portal del cliente...).",
            "detects": "Un pago nuevo y válido.",
            "schema": "PaymentWithCustomer",
            "dataSchema": {
                "$ref": "#/components/schemas/PaymentWithCustomer"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "payment.registered",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "id": "5501",
                    "invoiceId": "9001",
                    "invoiceNumber": "F001-0000123",
                    "date": "2026-10-06 14:30:00",
                    "amount": 80,
                    "currency": "PEN",
                    "method": "YAPE",
                    "reference": "00845122",
                    "status": "valid",
                    "customer": {
                        "id": "1234",
                        "name": "Ana Pérez"
                    }
                }
            }
        },
        {
            "type": "invoice.created",
            "label": "Factura emitida",
            "description": "Se emitió una factura o recibo a un cliente.",
            "detects": "Una factura nueva.",
            "schema": "WebhookInvoice",
            "dataSchema": {
                "$ref": "#/components/schemas/WebhookInvoice"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "invoice.created",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "id": "9002",
                    "number": "F001-0000124",
                    "issuedAt": "2026-10-06",
                    "dueDate": "2026-10-21",
                    "amount": 80,
                    "paid": 0,
                    "remaining": 80,
                    "currency": "PEN",
                    "status": "pending",
                    "description": "Mensualidad octubre",
                    "customer": {
                        "id": "1234",
                        "name": "Ana Pérez"
                    }
                }
            }
        },
        {
            "type": "customer.updated",
            "label": "Cliente modificado",
            "description": "Cambiaron los datos del cliente (nombre, documento, celulares, correo, dirección o zona).",
            "detects": "Un cambio en esos datos. Los clientes nuevos no generan este aviso.",
            "schema": "WebhookCustomer",
            "dataSchema": {
                "$ref": "#/components/schemas/WebhookCustomer"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "customer.updated",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "customer": {
                        "id": "1234",
                        "name": "Ana Pérez",
                        "phones": [
                            "51987654321"
                        ],
                        "document": "4****678",
                        "status": "active",
                        "plan": "Fibra 100",
                        "balance": {
                            "amount": 80,
                            "currency": "PEN",
                            "overdueInvoices": 1
                        },
                        "nextDueDate": "2026-10-15",
                        "zone": "Zona Norte"
                    }
                }
            }
        },
        {
            "type": "service.suspended",
            "label": "Servicio cortado",
            "description": "El servicio del cliente pasó a cortado (por deuda, a mano o por la API).",
            "detects": "El estado del servicio cambió a cortado.",
            "schema": "WebhookService",
            "dataSchema": {
                "$ref": "#/components/schemas/WebhookService"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "service.suspended",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "customer": {
                        "id": "1234",
                        "name": "Ana Pérez"
                    },
                    "service": {
                        "status": "suspended",
                        "previousStatus": "active"
                    }
                }
            }
        },
        {
            "type": "service.activated",
            "label": "Servicio activado",
            "description": "El servicio del cliente volvió a estar activo después de un corte, o terminó su instalación.",
            "detects": "El estado del servicio cambió de cortado o en instalación a activo.",
            "schema": "WebhookService",
            "dataSchema": {
                "$ref": "#/components/schemas/WebhookService"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "service.activated",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "customer": {
                        "id": "1234",
                        "name": "Ana Pérez"
                    },
                    "service": {
                        "status": "active",
                        "previousStatus": "suspended"
                    }
                }
            }
        },
        {
            "type": "ticket.created",
            "label": "Ticket creado",
            "description": "Se creó un ticket de soporte para un cliente.",
            "detects": "Un ticket nuevo.",
            "schema": "WebhookTicket",
            "dataSchema": {
                "$ref": "#/components/schemas/WebhookTicket"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "ticket.created",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "ticket": {
                        "id": "3021",
                        "number": "T-3021",
                        "type": "AVERIA INTERNET",
                        "status": "open",
                        "stage": "pending",
                        "priority": "high",
                        "description": "Sin internet desde la mañana",
                        "scheduledAt": "2026-10-06 09:12:00",
                        "openedAt": null,
                        "closedAt": null,
                        "createdAt": "2026-10-06 09:12:00",
                        "technician": null,
                        "resolution": null,
                        "customer": {
                            "id": "1234",
                            "name": "Ana Pérez"
                        }
                    }
                }
            }
        },
        {
            "type": "ticket.closed",
            "label": "Ticket cerrado",
            "description": "Se cerró (resolvió) un ticket.",
            "detects": "Un ticket que pasó a resuelto.",
            "schema": "WebhookTicket",
            "dataSchema": {
                "$ref": "#/components/schemas/WebhookTicket"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "ticket.closed",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "ticket": {
                        "id": "3021",
                        "number": "T-3021",
                        "type": "AVERIA INTERNET",
                        "status": "closed",
                        "stage": "resolved",
                        "priority": "high",
                        "description": "Sin internet desde la mañana",
                        "scheduledAt": "2026-10-06 09:12:00",
                        "openedAt": "2026-10-06 10:00:00",
                        "closedAt": "2026-10-06 11:30:00",
                        "createdAt": "2026-10-06 09:12:00",
                        "technician": "Luis Rojas",
                        "resolution": {
                            "comment": "Se cambió el conector",
                            "closedAt": "2026-10-06 11:30:00"
                        },
                        "customer": {
                            "id": "1234",
                            "name": "Ana Pérez"
                        }
                    }
                }
            }
        },
        {
            "type": "onu.offline",
            "label": "ONU caída",
            "description": "La ONU de un cliente dejó de estar en línea según el monitor de la OLT.",
            "detects": "Una caída que registró el monitor de la OLT (se revisa cada 5 minutos).",
            "schema": "WebhookOnu",
            "dataSchema": {
                "$ref": "#/components/schemas/WebhookOnu"
            },
            "example": {
                "id": "evt_9f2c4a1b7d3e5f60a1b2c3d4",
                "type": "onu.offline",
                "apiVersion": "v1",
                "createdAt": "2026-10-06T14:30:05-05:00",
                "data": {
                    "onu": {
                        "serial": "TPLG12345678",
                        "olt": "OLT-1",
                        "pon": "1/1/3",
                        "state": "offline",
                        "cause": "LOS",
                        "rxDbm": -27.9,
                        "rxLevel": "critical",
                        "at": "2026-10-06 14:25:00"
                    },
                    "customer": {
                        "id": "1234",
                        "name": "Ana Pérez"
                    }
                }
            }
        }
    ],
    "x-webhook-signature": {
        "header": "X-Wificor-Signature",
        "format": "t=<unix>,v1=<hex>",
        "algorithm": "HMAC-SHA256 de \"<t>.<cuerpo exacto>\" con el secreto del webhook, en hexadecimal",
        "toleranceSeconds": 300,
        "headers": [
            {
                "name": "X-Wificor-Signature",
                "description": "La firma: t=<unix>,v1=<hex>."
            },
            {
                "name": "X-Wificor-Event",
                "description": "El tipo del evento (igual que `type` del cuerpo)."
            },
            {
                "name": "X-Wificor-Delivery",
                "description": "Número de esta entrega (sirve para soporte)."
            },
            {
                "name": "X-Wificor-Timestamp",
                "description": "El mismo `t` de la firma, en segundos Unix."
            }
        ],
        "retry": {
            "minutes": [
                1,
                5,
                30,
                120,
                360
            ],
            "maxAttempts": 6,
            "disableAfter": 30
        },
        "snippets": {
            "PHP": "$body = file_get_contents('php://input');   // el cuerpo EXACTO, sin tocarlo\nparse_str(str_replace(',', '&', $_SERVER['HTTP_X_WIFICOR_SIGNATURE'] ?? ''), $p);\n$ok = isset($p['t'], $p['v1'])\n    && abs(time() - (int)$p['t']) <= 300\n    && hash_equals(hash_hmac('sha256', $p['t'] . '.' . $body, $SECRET), $p['v1']);\nif (!$ok) { http_response_code(400); exit; }\nhttp_response_code(200);   // responde 2xx rápido; procesa después",
            "Node.js": "const crypto = require('crypto');\n// rawBody: el cuerpo EXACTO (string o Buffer), antes de convertirlo a JSON\nfunction verify(rawBody, header, secret) {\n  const p = Object.fromEntries(header.split(',').map(s => s.split('=')));\n  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;\n  const expected = crypto.createHmac('sha256', secret).update(p.t + '.' + rawBody).digest('hex');\n  return !!p.v1 && p.v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));\n}",
            "Python": "import hmac, hashlib, time\n\ndef verify(raw_body: bytes, header: str, secret: str) -> bool:\n    p = dict(x.split('=', 1) for x in header.split(','))\n    if abs(time.time() - int(p['t'])) > 300:\n        return False\n    expected = hmac.new(secret.encode(), p['t'].encode() + b'.' + raw_body, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, p.get('v1', ''))"
        }
    },
    "x-scopes": [
        {
            "name": "customers:read",
            "label": "Clientes: consultar",
            "available": true
        },
        {
            "name": "billing:read",
            "label": "Facturas y deuda",
            "available": true
        },
        {
            "name": "payments:read",
            "label": "Medios de pago",
            "available": true
        },
        {
            "name": "payments:write",
            "label": "Pagos: promesas y comprobantes",
            "available": true
        },
        {
            "name": "payments:register",
            "label": "Pagos: registrar cobros",
            "available": true
        },
        {
            "name": "service:read",
            "label": "Estado del servicio",
            "available": true
        },
        {
            "name": "service:activate",
            "label": "Reactivar servicio",
            "available": true
        },
        {
            "name": "service:suspend",
            "label": "Cortar servicio",
            "available": true
        },
        {
            "name": "tickets:read",
            "label": "Tickets: consultar",
            "available": true
        },
        {
            "name": "tickets:write",
            "label": "Tickets: crear",
            "available": true
        },
        {
            "name": "tickets:close",
            "label": "Tickets: cerrar",
            "available": true
        },
        {
            "name": "network:read",
            "label": "Consumo de red",
            "available": true
        },
        {
            "name": "onu:read",
            "label": "ONU y OLT: consultar",
            "available": true
        },
        {
            "name": "onu:write",
            "label": "ONU: reiniciar",
            "available": true
        }
    ],
    "x-errors": [
        {
            "code": "UNAUTHORIZED",
            "status": 401,
            "description": "Falta la llave, no es válida, fue revocada o ya venció."
        },
        {
            "code": "FORBIDDEN",
            "status": 403,
            "description": "El perfil del usuario de la llave no permite la operación, o el servicio está suspendido por licencia."
        },
        {
            "code": "INSUFFICIENT_SCOPE",
            "status": 403,
            "description": "La llave no tiene el scope que exige este endpoint."
        },
        {
            "code": "NOT_FOUND",
            "status": 404,
            "description": "El recurso no existe (o la ruta no existe)."
        },
        {
            "code": "INVALID_REQUEST",
            "status": 400,
            "description": "Faltan datos o tienen un formato inválido."
        },
        {
            "code": "CONFLICT",
            "status": 409,
            "description": "La operación no se puede hacer en el estado actual."
        },
        {
            "code": "RATE_LIMITED",
            "status": 429,
            "description": "Pasaste el límite de uso de la llave. Espera lo que indica `Retry-After` (segundos)."
        },
        {
            "code": "UNAVAILABLE",
            "status": 500,
            "description": "No se pudo completar. 503 si el conector no está disponible en este sistema. Se puede reintentar más tarde."
        }
    ],
    "x-rate-limit-per-minute": 120
}