{"openapi":"3.1.0","info":{"title":"API de plataforma de TotalPro.es","version":"1.1.2","description":"API de plataforma que comunica **totalpro.es** (cuentas, monedero, cobro de la IA y tickets) con **data.totalpro.es** (agente TotalPro, consola remota) en los dos sentidos.\n\n**totalpro.es es la fuente de verdad** de usuarios, contraseñas y saldo. La IA se paga por uso desde el monedero, en euros: data.totalpro.es autentica contra esta API, comprueba `can_use_ai` antes de llamar al modelo, informa de cada petición (`POST /usage`, que la cobra) y recibe los cambios por webhook.\n\n## Cobro de la IA\n- Precio por modelo y millón de tokens (entrada, entrada cacheada, salida) en `GET /ai/prices`. Modelo no reconocido = tarifa más cara.\n- Orden de gasto: regalo de bienvenida (2 €, caduca a los 30 días) → saldo del monedero. Límite de gasto mensual (20 € por defecto, el usuario lo cambia).\n- `available_eur` / `can_use_ai` en `GET /me/entitlements` o `GET /users/{id}/entitlements` (cachéalo como mucho 60 s; los webhooks `entitlements.updated`, `ai.balance_low` y `ai.blocked` avisan de los cambios).\n- **Diagnóstico:** se cobra como cualquier otra petición. Los usuarios nuevos tienen el regalo de bienvenida para probarlo; después, su monedero (`can_use_ai`).\n\n## Tickets de soporte\nEl asistente hace el diagnóstico; si no lo resuelve, crea el ticket (`POST /me/tickets`, `POST /tickets` o `conversation.escalated`) con `internal_report`. Un técnico de TotalPro lo estudia (con la ayuda de la IA; puede llevar horas) y le pone precio (`ticket.quoted`, con `pay_url`), el cliente paga desde el monedero y un técnico lo resuelve por la consola remota. Si no se resuelve, se devuelve el importe al monedero (estado `refunded` si es total). Los tickets son privados.\n\n## Autenticación\n| Quién | Cabecera | Cómo se obtiene |\n|---|---|---|\n| Agente / consola (usuario) | `Authorization: Bearer tp_at_…` | `POST /auth/login` (dura 1 h; renovar con `POST /auth/refresh`) |\n| Servidor (data.totalpro.es) | `Authorization: Bearer tp_sk_…` | Panel de administración de totalpro.es → API (permisos por clave) |\n\nLas claves y tokens se guardan solo como hash. El refresh token es de un solo uso: reutilizarlo revoca toda la sesión.\n\n## Webhooks (totalpro.es → data.totalpro.es)\nCada evento se envía con `POST` y JSON (esquema `WebhookEvent`) y con estas cabeceras:\n- `TotalPro-Event`: tipo de evento · `TotalPro-Event-Id`: id único (para no procesarlo dos veces)\n- `TotalPro-Signature: t=<unix>,v1=<firma>`, donde `firma = HMAC-SHA256(secreto_webhook, \"<t>.<cuerpo>\")` en hexadecimal.\n\nRechaza firmas que no coincidan o con más de 5 minutos de antigüedad. Responde 2xx en menos de 5 s; si no, se reintenta con espera creciente (1 min, 2, 4… hasta 12 h) durante unos 2 días.\n\n## Errores\nFormato RFC 9457 (`application/problem+json`) con `title` estable para programar (`invalid_credentials`, `invalid_token`, `token_reused`, `user_blocked`, `insufficient_scope`, `too_many_attempts`, `validation_error`, `not_found`…) y `detail` legible.","contact":{"name":"TotalPro.es","email":"hola@totalpro.es","url":"https://totalpro.es/desarrolladores"}},"servers":[{"url":"https://totalpro.es/api/v1","description":"Producción"}],"tags":[{"name":"General"},{"name":"Agente y consola","description":"Llamadas con el token del usuario (agente TotalPro, consola remota)."},{"name":"Servidor a servidor","description":"Llamadas de data.totalpro.es con clave de API y permisos."}],"paths":{"/health":{"get":{"tags":["General"],"summary":"Estado del servicio","description":"Sin autenticación.","operationId":"health","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"api_version":{"type":"string"},"time":{"type":"string","format":"date-time"}}}}}}},"security":[]}},"/ai/prices":{"get":{"tags":["General"],"summary":"Precios de la IA por modelo","description":"Sin autenticación. Precio final que paga el usuario, en euros por millón de tokens. Incluye las tarifas futuras ya anunciadas (`valid_from`). Los modelos no listados se cobran con la tarifa `*`, la más cara: usa los nombres de modelo de esta lista.","operationId":"getAiPrices","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string"},"unit":{"type":"string"},"note":{"type":"string"},"data":{"type":"array","items":{"$ref":"#/components/schemas/ModelPrice"}}}}}}}},"security":[]}},"/openapi.json":{"get":{"tags":["General"],"summary":"Esta especificación","description":"Especificación OpenAPI 3.1 en JSON.","operationId":"openapi","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":[]}}}}},"security":[]}},"/auth/login":{"post":{"tags":["Agente y consola"],"summary":"Iniciar sesión","description":"El agente o la consola envían las credenciales de totalpro.es y reciben un access token (1 h) y un refresh token rotatorio (60 días). Si se envía `device`, se registra el equipo (reutilizando el mismo `machine_id`).","operationId":"login","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Tokens"},{"type":"object","properties":{"device_id":{"type":["integer","null"]},"user":{"$ref":"#/components/schemas/User"}}}]}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"},"device":{"$ref":"#/components/schemas/Device"}},"required":["email","password"]},"example":{"email":"laura@empresa.es","password":"********","device":{"machine_id":"7c2e…","name":"PORTATIL-LAURA","os":"Windows 11 24H2","agent_version":"0.3.0"}}}}}}},"/auth/refresh":{"post":{"tags":["Agente y consola"],"summary":"Renovar tokens","description":"Devuelve un nuevo par de tokens e invalida el anterior. Si se reutiliza un refresh token ya usado, se revoca toda la cadena (posible robo) y hay que volver a iniciar sesión.","operationId":"refresh","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Tokens"},{"type":"object","properties":{"device_id":{"type":["integer","null"]}}}]}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"422":{"$ref":"#/components/responses/E422"}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"refresh_token":{"type":"string"}},"required":["refresh_token"]}}}}}},"/auth/logout":{"post":{"tags":["Agente y consola"],"summary":"Cerrar sesión","description":"Revoca la sesión actual o, con `all_devices: true`, todas las del usuario. Emite `session.revoked`.","operationId":"logout","responses":{"204":{"description":"Sesión cerrada"},"401":{"$ref":"#/components/responses/E401"}},"security":[{"userToken":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"all_devices":{"type":"boolean"}}}}}}}},"/me":{"get":{"tags":["Agente y consola"],"summary":"Usuario actual","description":"Perfil, saldo para la IA y saldo del monedero.","operationId":"getMe","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}},"security":[{"userToken":[]}]}},"/me/entitlements":{"get":{"tags":["Agente y consola"],"summary":"Saldo para la IA","description":"Lo que puede gastar en IA, regalo vigente, límite mensual y gasto del mes.","operationId":"getMyEntitlements","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlements"}}}},"401":{"$ref":"#/components/responses/E401"}},"security":[{"userToken":[]}]},"patch":{"tags":["Agente y consola"],"summary":"Cambiar mi límite de gasto mensual","description":"`monthly_limit_eur`: importe en euros, `null` = sin límite, `\"default\"` = el de la plataforma. Emite `entitlements.updated`.","operationId":"setMyLimit","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlements"}}}},"401":{"$ref":"#/components/responses/E401"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"userToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"monthly_limit_eur":{"type":["number","string","null"],"minimum":0,"maximum":10000}},"required":["monthly_limit_eur"]},"example":{"monthly_limit_eur":30}}}}}},"/me/usage":{"get":{"tags":["Agente y consola"],"summary":"Gasto de IA del usuario","description":"Gasto por día, por modelo y por conversación en un mes.","operationId":"getMyUsage","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummary"}}}},"401":{"$ref":"#/components/responses/E401"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"userToken":[]}],"parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$","example":"2026-10"},"description":"Mes (AAAA-MM). Por defecto, el actual."}]}},"/me/devices":{"get":{"tags":["Agente y consola"],"summary":"Equipos del usuario","description":"Equipos con sesión activa del agente.","operationId":"listMyDevices","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DeviceOut"}}}}}}},"401":{"$ref":"#/components/responses/E401"}},"security":[{"userToken":[]}]}},"/me/devices/{device_id}":{"delete":{"tags":["Agente y consola"],"summary":"Desvincular un equipo","description":"Revoca el equipo y sus tokens. Emite `session.revoked`.","operationId":"revokeMyDevice","responses":{"204":{"description":"Equipo desvinculado"},"401":{"$ref":"#/components/responses/E401"},"404":{"$ref":"#/components/responses/E404"}},"security":[{"userToken":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"integer"}}]}},"/me/tickets":{"get":{"tags":["Agente y consola"],"summary":"Tickets del usuario","description":"Los 100 más recientes.","operationId":"listMyTickets","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Ticket"}}}}}}},"401":{"$ref":"#/components/responses/E401"}},"security":[{"userToken":[]}]},"post":{"tags":["Agente y consola"],"summary":"Abrir un ticket desde el agente","description":"Crea un ticket en nombre del usuario cuando el diagnóstico de la IA no lo resuelve, con su `internal_report` para el técnico. El equipo de TotalPro le pone precio (`ticket.quoted`, con `pay_url`), el cliente paga y un técnico lo resuelve. Emite `ticket.created`.","operationId":"createMyTicket","responses":{"201":{"description":"Ticket creado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"401":{"$ref":"#/components/responses/E401"},"413":{"$ref":"#/components/responses/E413"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"userToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketCreate"}}}}}},"/me/tickets/{ticket_id}":{"get":{"tags":["Agente y consola"],"summary":"Un ticket del usuario","description":"","operationId":"getMyTicket","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"401":{"$ref":"#/components/responses/E401"},"404":{"$ref":"#/components/responses/E404"}},"security":[{"userToken":[]}],"parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"integer"}}]}},"/auth/verify":{"post":{"tags":["Servidor a servidor"],"summary":"Verificar credenciales","description":"Para el login de data.totalpro.es (`/v1/auth/login` y `/v1/auth/agent_login`): comprueba email y contraseña contra totalpro.es y devuelve el usuario con su saldo para la IA. Con `issue_tokens: true` devuelve también tokens de usuario. Limitado a 8 intentos fallidos por email e IP cada 15 minutos.","operationId":"verifyCredentials","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"},"device_id":{"type":["integer","null"]},"tokens":{"$ref":"#/components/schemas/Tokens"}},"required":["user"]}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"422":{"$ref":"#/components/responses/E422"},"429":{"$ref":"#/components/responses/E429"}},"security":[{"apiKey":["auth:verify"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","format":"password"},"device":{"$ref":"#/components/schemas/Device"},"issue_tokens":{"type":"boolean","default":false},"client_ip":{"type":"string","description":"IP real del usuario final. Los intentos fallidos se limitan por email + esta IP (si no se envía, por la IP del servidor que llama)."}},"required":["email","password"]}}}}}},"/auth/exchange_code":{"post":{"tags":["Servidor a servidor"],"summary":"Canjear el código del login delegado","description":"Login con la cuenta de totalpro.es (Google o contraseña) desde el agente o la consola, sin que pasen las credenciales por data.totalpro.es. El agente abre en el navegador `https://totalpro.es/auth/agent?client=<nombre del cliente de API>&state=<state>&redirect_uri=<uri>&scope=agent|dashboard`. `redirect_uri` debe estar, exacta, en la lista blanca del cliente (panel de administración → API). El usuario entra y **confirma** (siempre, aunque ya tenga sesión); se vuelve a `redirect_uri?state=…&code=tpc_…` o, si cancela, `redirect_uri?state=…&error=access_denied`. Este endpoint canjea el código (un solo uso, caduca a los 5 minutos, ligado al `state` y al cliente que lo pidió). Respuesta idéntica a `POST /auth/verify`.","operationId":"exchangeCode","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"},"device_id":{"type":["integer","null"]},"tokens":{"$ref":"#/components/schemas/Tokens"}},"required":["user"]}}}},"400":{"$ref":"#/components/responses/E400"},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"apiKey":["auth:verify"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","example":"tpc_…"},"state":{"type":"string"},"client_ip":{"type":"string","description":"IP real del usuario final"},"device":{"$ref":"#/components/schemas/Device"},"issue_tokens":{"type":"boolean","default":false}},"required":["code","state"]},"example":{"code":"tpc_…","state":"st_9f2c…","client_ip":"83.45.12.9"}}}}}},"/auth/introspect":{"post":{"tags":["Servidor a servidor"],"summary":"Validar un token de usuario","description":"Estilo RFC 7662. Devuelve `active: false` si el token no existe, ha caducado, está revocado o el usuario está bloqueado.","operationId":"introspectToken","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"active":{"type":"boolean"},"token_type":{"type":"string","enum":["access","refresh"]},"user_id":{"type":"integer"},"device_id":{"type":["integer","null"]},"exp":{"type":"integer"}},"required":["active"]}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"}},"security":[{"apiKey":["auth:verify"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]}}}}}},"/users":{"get":{"tags":["Servidor a servidor"],"summary":"Buscar usuario por email","description":"","operationId":"findUserByEmail","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"apiKey":["users:read"]}],"parameters":[{"name":"email","in":"query","required":true,"schema":{"type":"string","format":"email"}}]}},"/users/{user_id}":{"get":{"tags":["Servidor a servidor"],"summary":"Leer usuario","description":"","operationId":"getUser","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}},"security":[{"apiKey":["users:read"]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer"},"description":"ID del usuario en totalpro.es"}]}},"/users/{user_id}/entitlements":{"get":{"tags":["Servidor a servidor"],"summary":"Saldo para la IA de un usuario","description":"","operationId":"getEntitlements","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlements"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}},"security":[{"apiKey":["users:read"]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer"},"description":"ID del usuario en totalpro.es"}]},"patch":{"tags":["Servidor a servidor"],"summary":"Cambiar el límite mensual o regalar crédito de IA","description":"`monthly_limit_eur`: importe, `null` = sin límite, `\"default\"` = el de la plataforma. `credit_add_eur` (0,01–100 €) regala crédito solo para IA que caduca a los `credit_expires_days` días (30 por defecto), p. ej. para compensar una incidencia. Emite `entitlements.updated`.","operationId":"updateEntitlements","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Entitlements"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"apiKey":["users:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"monthly_limit_eur":{"type":["number","string","null"]},"credit_add_eur":{"type":"number","minimum":0.01,"maximum":100},"credit_expires_days":{"type":"integer","minimum":1,"maximum":365,"default":30},"note":{"type":"string","maxLength":255}}},"example":{"credit_add_eur":1.5,"credit_expires_days":30,"note":"Compensación por caída del servicio"}}}},"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer"},"description":"ID del usuario en totalpro.es"}]}},"/users/{user_id}/usage":{"get":{"tags":["Servidor a servidor"],"summary":"Gasto de IA de un usuario","description":"","operationId":"getUserUsage","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageSummary"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"apiKey":["users:read"]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer"},"description":"ID del usuario en totalpro.es"},{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}$","example":"2026-10"},"description":"Mes (AAAA-MM). Por defecto, el actual."}]}},"/usage":{"post":{"tags":["Servidor a servidor"],"summary":"Registrar y cobrar consumo de IA","description":"data.totalpro.es informa de cada petición al modelo (hasta 500 por llamada; mejor al terminar cada respuesta). totalpro.es calcula el precio con la tabla de `GET /ai/prices` y lo cobra: primero del regalo y luego del saldo del monedero. Idempotente por `id`. Una petición ya hecha se cobra siempre; si después `can_use_ai` es false, data.totalpro.es debe dejar de llamar al modelo para ese usuario. Al bajar de 1 € se emite `ai.balance_low` y al quedarse sin poder usar la IA, `ai.blocked`.","operationId":"recordUsage","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/UsageResult"}}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"413":{"$ref":"#/components/responses/E413"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"apiKey":["usage:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"records":{"type":"array","maxItems":500,"items":{"$ref":"#/components/schemas/UsageRecord"}}},"required":["records"]},"example":{"records":[{"id":"req_01HZ…","user_id":42,"model":"gemini-3.8-flash","input_tokens":12400,"cached_input_tokens":8000,"output_tokens":650,"thinking_tokens":300,"occurred_at":"2026-10-11T10:15:00+02:00","machine_id":"7c2e…","conversation_id":"conv_9"}]}}}}}},"/tickets":{"post":{"tags":["Servidor a servidor"],"summary":"Crear ticket en nombre de un usuario","description":"Igual que `POST /me/tickets`, indicando `user_id`.","operationId":"createTicket","responses":{"201":{"description":"Ticket creado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"},"413":{"$ref":"#/components/responses/E413"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"apiKey":["tickets:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/TicketCreate"},{"type":"object","properties":{"user_id":{"type":"integer"}},"required":["user_id"]}]}}}}}},"/tickets/{ticket_id}":{"get":{"tags":["Servidor a servidor"],"summary":"Leer ticket","description":"","operationId":"getTicket","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticket"}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"404":{"$ref":"#/components/responses/E404"}},"security":[{"apiKey":["tickets:read"]}],"parameters":[{"name":"ticket_id","in":"path","required":true,"schema":{"type":"integer"}}]}},"/events":{"post":{"tags":["Servidor a servidor"],"summary":"Enviar un evento a totalpro.es","description":"Comunicación de data.totalpro.es hacia totalpro.es. Idempotente por `id`. Si el cliente tiene secreto de entrada configurado, la cabecera `TotalPro-Signature` es obligatoria (mismo formato que los webhooks). `conversation.escalated` con `data.create_ticket: true` abre un ticket; `machine.registered` / `machine.removed` mantienen la lista de equipos del usuario.","operationId":"postEvent","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"received":{"type":"boolean"},"duplicate":{"type":"boolean"},"device_id":{"type":"integer"},"ticket":{"$ref":"#/components/schemas/Ticket"}}}}}},"401":{"$ref":"#/components/responses/E401"},"403":{"$ref":"#/components/responses/E403"},"422":{"$ref":"#/components/responses/E422"}},"security":[{"apiKey":["events:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InboundEvent"},"example":{"id":"evt_8812","type":"conversation.escalated","user_id":42,"data":{"conversation_id":"conv_9","create_ticket":true,"title":"Outlook no sincroniza","description":"La IA ha recreado el perfil y sigue sin sincronizar.","category":"correo"}}}}}}},"/webhooks/deliveries":{"get":{"tags":["Servidor a servidor"],"summary":"Historial de webhooks","description":"Últimas 100 entregas a este cliente.","operationId":"listDeliveries","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"status":{"type":"string","enum":["pending","delivered","failed"]},"attempts":{"type":"integer"},"last_status_code":{"type":["integer","null"]},"last_error":{"type":["string","null"]},"created_at":{"type":"string"},"delivered_at":{"type":["string","null"]}}}}}}}}},"401":{"$ref":"#/components/responses/E401"}},"security":[{"apiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","delivered","failed"]}}]}},"/webhooks/test":{"post":{"tags":["Servidor a servidor"],"summary":"Enviar un webhook de prueba","description":"Envía un evento `ping` firmado a la URL configurada y devuelve el resultado.","operationId":"testWebhook","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"last_status_code":{"type":["integer","null"]},"last_error":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/E401"},"409":{"$ref":"#/components/responses/E409"}},"security":[{"apiKey":[]}]}}},"webhooks":{"user.created":{"post":{"summary":"Usuario registrado","description":"Evento `user.created` enviado por totalpro.es a la URL del cliente.","operationId":"wh_user_created","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"user.updated":{"post":{"summary":"Datos del usuario cambiados","description":"Evento `user.updated` enviado por totalpro.es a la URL del cliente.","operationId":"wh_user_updated","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"user.blocked":{"post":{"summary":"Usuario bloqueado: cerrar sus sesiones y agentes","description":"Evento `user.blocked` enviado por totalpro.es a la URL del cliente.","operationId":"wh_user_blocked","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"user.unblocked":{"post":{"summary":"Usuario desbloqueado","description":"Evento `user.unblocked` enviado por totalpro.es a la URL del cliente.","operationId":"wh_user_unblocked","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"entitlements.updated":{"post":{"summary":"Cambia lo que puede gastar en IA (recarga, regalo, límite, ajuste). data.reason: topup | gift | limit_changed | admin | api","description":"Evento `entitlements.updated` enviado por totalpro.es a la URL del cliente.","operationId":"wh_entitlements_updated","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"ai.balance_low":{"post":{"summary":"Queda poco saldo para la IA (avisar al usuario)","description":"Evento `ai.balance_low` enviado por totalpro.es a la URL del cliente.","operationId":"wh_ai_balance_low","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"ai.blocked":{"post":{"summary":"Ya no puede usar la IA: data.reason = no_balance | monthly_limit. Parar las llamadas al modelo","description":"Evento `ai.blocked` enviado por totalpro.es a la URL del cliente.","operationId":"wh_ai_blocked","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"session.revoked":{"post":{"summary":"Sesión o equipo revocado","description":"Evento `session.revoked` enviado por totalpro.es a la URL del cliente.","operationId":"wh_session_revoked","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"ticket.created":{"post":{"summary":"Ticket creado","description":"Evento `ticket.created` enviado por totalpro.es a la URL del cliente.","operationId":"wh_ticket_created","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"ticket.quoted":{"post":{"summary":"El equipo envía (o cambia) el precio del ticket: avisar al cliente en el agente con el enlace pay_url","description":"Evento `ticket.quoted` enviado por totalpro.es a la URL del cliente.","operationId":"wh_ticket_quoted","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}},"ticket.status_changed":{"post":{"summary":"Cambio de estado de un ticket (pagado, resuelto, devuelto, cancelado)","description":"Evento `ticket.status_changed` enviado por totalpro.es a la URL del cliente.","operationId":"wh_ticket_status_changed","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookEvent"}}}},"responses":{"2XX":{"description":"Recibido. Cualquier otro código provoca reintentos."}}}}},"components":{"securitySchemes":{"userToken":{"type":"http","scheme":"bearer","bearerFormat":"tp_at_…","description":"Access token de usuario (POST /auth/login)."},"apiKey":{"type":"http","scheme":"bearer","bearerFormat":"tp_sk_…","description":"Clave de API de servidor. Permisos: auth:verify, users:read, users:write, usage:write, tickets:read, tickets:write, events:write."}},"schemas":{"Problem":{"type":"object","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string","description":"Código de error estable (p. ej. invalid_credentials)"},"status":{"type":"integer"},"detail":{"type":"string","description":"Mensaje legible en español"}},"required":["title","status","detail"],"description":"Error con formato RFC 9457 (application/problem+json).","example":{"type":"https://totalpro.es/desarrolladores#errores","title":"invalid_credentials","status":401,"detail":"Email o contraseña incorrectos."}},"Entitlements":{"type":"object","properties":{"currency":{"type":"string","enum":["EUR"]},"can_use_ai":{"type":"boolean","description":"Si es false, NO llamar al modelo de IA para este usuario (mostrar el motivo y el enlace a recargar)"},"blocked_reason":{"type":["string","null"],"enum":["no_balance","monthly_limit","user_blocked",null],"description":"no_balance: sin saldo · monthly_limit: ha llegado a su límite de gasto mensual · user_blocked: cuenta bloqueada"},"available_eur":{"type":"number","example":12.3456,"description":"Lo que puede gastar ahora en IA: regalo vigente + saldo (sin pasar del límite mensual)"},"low_balance":{"type":"boolean","description":"Disponible por debajo del umbral de aviso (1 € por defecto)"},"gift":{"type":["object","null"],"properties":{"remaining_eur":{"type":"number"},"expires_at":{"type":"string","format":"date-time"}},"description":"Crédito de IA regalado (bienvenida, compensaciones). Se gasta antes que el saldo. null si no tiene."},"balance_eur":{"type":"number","description":"Saldo del monedero descontando el consumo de IA aún no cobrado (puede ser ligeramente negativo)"},"monthly_limit_eur":{"type":["number","null"],"description":"Límite de gasto mensual en IA (solo saldo, el regalo no cuenta). null = sin límite"},"monthly_limit_is_default":{"type":"boolean","description":"true si el usuario no ha fijado su propio límite"},"limit_remaining_eur":{"type":["number","null"]},"period":{"type":"string","example":"2026-10"},"period_resets_at":{"type":"string","format":"date-time"},"spent_period_eur":{"type":"number","description":"Gastado este mes del saldo"},"spent_period_gift_eur":{"type":"number","description":"Gastado este mes del regalo"},"tokens_used_period":{"type":"integer","description":"Tokens usados este mes (informativo)"},"plan":{"type":"string","enum":["pay_as_you_go"],"deprecated":true},"plan_label":{"type":"string","deprecated":true},"tokens_available":{"type":"integer","deprecated":true,"description":"OBSOLETO: estimación prudente de tokens que puede gastar; 0 si can_use_ai es false. Usar can_use_ai / available_eur"},"token_limit_monthly":{"type":"integer","deprecated":true},"tokens_extra":{"type":"integer","deprecated":true},"custom_limit":{"type":"boolean","deprecated":true}},"required":["currency","can_use_ai","available_eur","balance_eur","period"],"description":"Lo que el usuario puede gastar en IA, en euros. La IA se paga por uso desde el monedero de totalpro.es (precio por modelo, ver GET /ai/prices). totalpro.es es la fuente de verdad: data.totalpro.es debe consultar `can_use_ai` antes de llamar al modelo.","example":{"currency":"EUR","can_use_ai":true,"blocked_reason":null,"available_eur":21.35,"low_balance":false,"gift":{"remaining_eur":1.35,"expires_at":"2026-11-10T15:06:23+01:00"},"balance_eur":54.85,"monthly_limit_eur":20,"monthly_limit_is_default":true,"limit_remaining_eur":20,"period":"2026-10","period_resets_at":"2026-11-01T00:00:00+01:00","spent_period_eur":0,"spent_period_gift_eur":0.65,"tokens_used_period":184000}},"ModelPrice":{"type":"object","properties":{"model":{"type":"string","example":"gemini-3.8-flash","description":"Nombre del modelo (\"*\" = tarifa para modelos no reconocidos)"},"label":{"type":"string"},"valid_from":{"type":["string","null"],"format":"date","description":"null = vigente hoy; con fecha = tarifa futura ya anunciada"},"input_eur_per_mtok":{"type":"number"},"cached_input_eur_per_mtok":{"type":["number","null"]},"output_eur_per_mtok":{"type":"number"},"long_context_threshold":{"type":["integer","null"],"description":"Si input_tokens supera este valor se aplican los precios long_*"},"long_input_eur_per_mtok":{"type":["number","null"]},"long_output_eur_per_mtok":{"type":["number","null"]}}},"Wallet":{"type":"object","properties":{"balance":{"type":"number","example":25.5},"currency":{"type":"string","example":"EUR"}},"required":["balance","currency"],"description":"Saldo del monedero de totalpro.es (euros), ya descontado el consumo de IA."},"User":{"type":"object","properties":{"id":{"type":"integer","example":42},"email":{"type":"string","format":"email"},"name":{"type":"string"},"user_type":{"type":"string","enum":["cliente","tecnico"]},"role":{"type":"string","enum":["user","admin"]},"active":{"type":"boolean","description":"false si está bloqueado"},"email_verified":{"type":"boolean"},"technician_status":{"type":["string","null"],"enum":["pending","approved","rejected",null]},"created_at":{"type":"string","format":"date-time"},"entitlements":{"$ref":"#/components/schemas/Entitlements"},"wallet":{"$ref":"#/components/schemas/Wallet"}},"required":["id","email","name","user_type","role","active","entitlements"]},"Device":{"type":"object","properties":{"machine_id":{"type":"string","maxLength":100,"description":"Identificador estable del equipo (el mismo que usa data.totalpro.es)"},"name":{"type":"string","maxLength":150,"example":"PORTATIL-LAURA"},"os":{"type":"string","maxLength":100,"example":"Windows 11 24H2"},"agent_version":{"type":"string","maxLength":30,"example":"0.3.0"}}},"DeviceOut":{"type":"object","properties":{"id":{"type":"integer"},"machine_id":{"type":["string","null"]},"name":{"type":["string","null"]},"os":{"type":["string","null"]},"agent_version":{"type":["string","null"]},"last_seen_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"Tokens":{"type":"object","properties":{"access_token":{"type":"string","example":"tp_at_…"},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer","example":3600},"refresh_token":{"type":"string","example":"tp_rt_…"},"refresh_expires_in":{"type":"integer","example":5184000}},"required":["access_token","token_type","expires_in","refresh_token"]},"Ticket":{"type":"object","properties":{"id":{"type":"integer"},"user_id":{"type":"integer"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string","enum":["equipos","correo","seguridad","redes","mantenimiento"]},"urgency":{"type":"string","enum":["low","medium","high"]},"status":{"type":"string","enum":["pending_review","quoted","in_progress","resolved","refunded","cancelled"],"description":"pending_review: un técnico lo está estudiando para ponerle precio · quoted: precio enviado, pendiente de pago (pay_url) · in_progress: pagado, un técnico se encarga · resolved · refunded: devuelto entero al monedero · cancelled"},"source":{"type":"string","enum":["web","agent","phone"]},"external_ref":{"type":["string","null"]},"url":{"type":"string","format":"uri","description":"Página privada del ticket (solo la ve su dueño, con sesión iniciada)"},"price":{"type":["object","null"],"properties":{"amount_eur":{"type":"number"},"note":{"type":["string","null"],"description":"Qué se va a hacer (lo ve el cliente)"},"quoted_at":{"type":["string","null"],"format":"date-time"}}},"pay_url":{"type":["string","null"],"format":"uri","description":"Solo en quoted: donde el cliente paga (enlázalo desde el agente)"},"paid_eur":{"type":["number","null"]},"refunded_eur":{"type":"number"},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"}},"description":"Ticket de soporte. Nunca incluye el informe interno ni los datos técnicos."},"TicketCreate":{"type":"object","properties":{"title":{"type":"string","maxLength":150,"example":"La impresora no imprime"},"description":{"type":"string","minLength":20,"example":"El agente ha reiniciado la cola de impresión pero sigue sin imprimir."},"category":{"type":"string","enum":["equipos","correo","seguridad","redes","mantenimiento"],"default":"equipos"},"urgency":{"type":"string","enum":["low","medium","high"],"default":"medium"},"external_ref":{"type":"string","maxLength":100,"description":"Referencia en data.totalpro.es (p. ej. id de conversación)"},"diagnostics":{"type":"object","properties":[],"description":"Datos técnicos del equipo recogidos por el agente (máx. 200 KB). Solo los ve el equipo de TotalPro."},"internal_report":{"type":"string","maxLength":100000,"description":"Informe del diagnóstico de la IA para el técnico (texto o Markdown, máx. 100 KB): qué cuenta el cliente, qué se ha comprobado y probado, causa probable, propuesta y tiempo estimado. El cliente NO lo ve."}},"required":["title","description"]},"UsageRecord":{"type":"object","properties":{"id":{"type":"string","maxLength":100,"description":"Id único de la petición en data.totalpro.es (idempotencia: reenviarla no la cobra dos veces)"},"user_id":{"type":"integer"},"model":{"type":"string","example":"gemini-3.8-flash","description":"Modelo exacto usado (se acepta \"models/…\" y sufijos de versión)"},"input_tokens":{"type":"integer","description":"Tokens de entrada, incluidos los cacheados (Gemini: promptTokenCount)"},"cached_input_tokens":{"type":"integer","description":"Parte de la entrada servida desde caché (cachedContentTokenCount); se cobra más barata"},"output_tokens":{"type":"integer","description":"Tokens de salida (candidatesTokenCount)"},"thinking_tokens":{"type":"integer","description":"Tokens de razonamiento (thoughtsTokenCount) si no van ya en output_tokens; se cobran como salida"},"total_tokens":{"type":"integer","description":"Solo si no se conoce el desglose: entonces todo se cobra a precio de salida"},"occurred_at":{"type":"string","format":"date-time"},"machine_id":{"type":"string"},"conversation_id":{"type":"string","description":"Para que el usuario vea el gasto por conversación"},"purpose":{"type":"string","enum":["diagnosis"],"description":"Opcional e informativo: \"diagnosis\" marca las peticiones del diagnóstico inicial para las estadísticas. Se cobran igual que el resto"}},"required":["id","user_id","model"]},"UsageResult":{"type":"object","properties":{"index":{"type":"integer"},"id":{"type":"string"},"status":{"type":"string","enum":["recorded","duplicate","rejected"]},"user_id":{"type":"integer"},"charged_eur":{"type":"number","description":"Importe cobrado por esta petición (0 si es duplicada)"},"priced_as":{"type":"string","description":"Tarifa aplicada (\"*\" si el modelo no se reconoce)"},"available_eur":{"type":"number"},"can_use_ai":{"type":"boolean"},"blocked_reason":{"type":["string","null"]},"over_limit":{"type":"boolean","description":"Igual a !can_use_ai: data.totalpro.es debe frenar las peticiones de IA de ese usuario"},"tokens_available":{"type":"integer","deprecated":true,"description":"OBSOLETO (API 1.0); usar available_eur"},"error":{"type":"string"}}},"UsageSummary":{"type":"object","properties":{"period":{"type":"string"},"currency":{"type":"string","enum":["EUR"]},"total_tokens":{"type":"integer"},"requests":{"type":"integer"},"charged_eur":{"type":"number","description":"Total del mes (regalo + saldo)"},"from_gift_eur":{"type":"number"},"by_day":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"tokens":{"type":"integer"},"input_tokens":{"type":"integer"},"output_tokens":{"type":"integer"},"requests":{"type":"integer"},"charged_eur":{"type":"number"},"from_gift_eur":{"type":"number"}}}},"by_model":{"type":"array","items":{"type":"object","properties":{"model":{"type":"string"},"tokens":{"type":"integer"},"requests":{"type":"integer"},"charged_eur":{"type":"number"}}}},"top_conversations":{"type":"array","items":{"type":"object","properties":{"conversation_id":{"type":"string"},"first_at":{"type":"string","format":"date-time"},"last_at":{"type":"string","format":"date-time"},"tokens":{"type":"integer"},"requests":{"type":"integer"},"charged_eur":{"type":"number"}}}}}},"InboundEvent":{"type":"object","properties":{"id":{"type":"string","maxLength":64,"description":"Id único del evento (idempotencia)"},"type":{"type":"string","enum":["machine.registered","machine.removed","alert.created","conversation.escalated","support.session_started","support.session_ended"]},"user_id":{"type":"integer"},"occurred_at":{"type":"string","format":"date-time"},"data":{"type":"object","properties":[],"description":"machine.registered: Device · conversation.escalated: {conversation_id, create_ticket, title, description, category, urgency, diagnostics, internal_report}"}},"required":["id","type"]},"WebhookEvent":{"type":"object","properties":{"id":{"type":"string","example":"evt_3f9a…"},"type":{"type":"string","enum":["ping","user.created","user.updated","user.blocked","user.unblocked","entitlements.updated","ai.balance_low","ai.blocked","session.revoked","ticket.created","ticket.quoted","ticket.status_changed"]},"created":{"type":"string","format":"date-time"},"api_version":{"type":"string","example":"1.1.0"},"data":{"type":"object","properties":[]}},"required":["id","type","created","data"],"description":"Cuerpo de cada webhook que totalpro.es envía al cliente (POST, JSON). Ver «Webhooks» en la descripción."}},"responses":{"E401":{"description":"No autenticado o credenciales no válidas","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"E403":{"description":"Sin permiso o usuario bloqueado","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"E404":{"description":"No encontrado","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"E409":{"description":"Conflicto","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"E413":{"description":"Petición demasiado grande","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"E422":{"description":"Datos no válidos","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"E429":{"description":"Demasiados intentos","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}