API de plataforma de TotalPro.es
Comunica totalpro.es (cuentas, monedero, cobro de la IA y tickets) con data.totalpro.es y el agente TotalPro, en los dos sentidos: llamadas REST hacia totalpro.es y webhooks firmados desde totalpro.es.
Agente y consola
Inician sesión con la cuenta de totalpro.es (POST /auth/login) y usan un token de usuario que dura 1 hora y se renueva solo.
data.totalpro.es
Verifica credenciales, comprueba que el usuario tiene saldo para la IA, informa de cada petición (totalpro.es la cobra del monedero) y abre tickets cuando la IA no lo resuelve.
Webhooks
totalpro.es avisa al momento de bloqueos, recargas, saldo bajo o agotado, sesiones revocadas y cambios en los tickets, con firma HMAC-SHA256.
Integración de data.totalpro.es en 5 pasos
URL base: https://totalpro.es/api/v1 · Todas las respuestas son JSON; los errores siguen el formato RFC 9457.
1. Login con la cuenta de totalpro.es
En /v1/auth/login y /v1/auth/agent_login, data.totalpro.es comprueba las credenciales contra totalpro.es y crea o actualiza su usuario local. El saldo para la IA viene en entitlements.
import httpx
TP = "https://totalpro.es/api/v1"
HEADERS = {"Authorization": f"Bearer {TOTALPRO_API_KEY}"} # tp_sk_… (variable de entorno)
r = httpx.post(f"{TP}/auth/verify", headers=HEADERS, json={
"email": email, "password": password,
"device": {"machine_id": machine_id, "name": hostname, "os": "Windows 11 24H2", "agent_version": "0.3.0"},
"client_ip": request.client.host, # IP real del usuario: limita los intentos fallidos por usuario, no por servidor
})
if r.status_code != 200:
raise HTTPException(401, r.json()["detail"]) # invalid_credentials, user_blocked, too_many_attempts…
user = r.json()["user"]
ent = user["entitlements"] # {"can_use_ai": true, "available_eur": 2.0, "gift": {...}, ...} 2. Comprobar el saldo y cobrar cada petición de IA
La IA se paga por uso desde el monedero, en euros, según el modelo (precios por modelo). Antes de llamar al modelo, comprueba can_use_ai; después, envía los tokens de la respuesta (los de usageMetadata de Gemini) y totalpro.es la cobra: primero del regalo de bienvenida y luego del saldo. Es idempotente por id. Si tras cobrar can_use_ai es false (sin saldo o límite mensual alcanzado), no hagas más llamadas al modelo para ese usuario y enséñale el enlace a https://totalpro.es/wallet.
ent = httpx.get(f"{TP}/users/{tp_user_id}/entitlements", headers=HEADERS).json() # o la caché (máx. 60 s)
if not ent["can_use_ai"]:
return sin_saldo(ent["blocked_reason"]) # no_balance | monthly_limit
resp = gemini.generate_content(...)
u = resp.usage_metadata
r = httpx.post(f"{TP}/usage", headers=HEADERS, json={"records": [{
"id": request_id, "user_id": tp_user_id, "model": "gemini-3.8-flash",
"input_tokens": u.prompt_token_count, "cached_input_tokens": u.cached_content_token_count or 0,
"output_tokens": u.candidates_token_count, "thinking_tokens": u.thoughts_token_count or 0,
"machine_id": machine_id, "conversation_id": conversation_id,
}]})
res = r.json()["results"][0] # {"charged_eur": 0.0042, "available_eur": 1.83, "can_use_ai": true, ...} 3. Abrir un ticket cuando la IA no lo resuelve
El ticket es privado. El equipo de TotalPro lee el informe interno de la IA y envía un precio cerrado (webhook ticket.quoted con pay_url); el cliente paga desde su monedero y un técnico lo resuelve.
httpx.post(f"{TP}/events", headers=HEADERS, json={
"id": f"esc-{conversation_id}", "type": "conversation.escalated", "user_id": tp_user_id,
"data": {"conversation_id": conversation_id, "create_ticket": True, "category": "correo",
"title": "Outlook no sincroniza", "description": lo_que_cuenta_el_cliente,
"internal_report": informe_para_el_tecnico, "diagnostics": inventario},
}) 4. Recibir webhooks y verificar la firma
Cabecera TotalPro-Signature: t=<unix>,v1=<hex>, con v1 = HMAC-SHA256(secreto, "<t>.<cuerpo>"). Responde 2xx en menos de 5 s; si no, se reintenta durante unos 2 días. Usa TotalPro-Event-Id para no procesar dos veces el mismo evento.
import hmac, hashlib, time
@app.post("/v1/hooks/totalpro")
async def totalpro_webhook(request: Request):
body = await request.body()
parts = dict(p.split("=", 1) for p in request.headers["TotalPro-Signature"].split(","))
expected = hmac.new(WEBHOOK_SECRET.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
if abs(time.time() - int(parts["t"])) > 300 or not hmac.compare_digest(expected, parts["v1"]):
raise HTTPException(400, "firma no válida")
event = json.loads(body)
match event["type"]:
case "user.blocked": desactivar_usuario(event["data"]["user_id"])
case "entitlements.updated": actualizar_saldo(event["data"]["user_id"], event["data"]["entitlements"])
case "ai.blocked": pausar_ia(event["data"]["user_id"], event["data"]["reason"])
case "ai.balance_low": avisar_saldo_bajo(event["data"]["user_id"])
case "session.revoked": cerrar_sesiones(event["data"])
case "ticket.status_changed": notificar_agente(event["data"]["ticket"])
return {"ok": True} 5. Agente: sesión del usuario
Si el agente habla directamente con totalpro.es, inicia sesión una vez y renueva el token antes de que caduque. El refresh token es de un solo uso.
curl -X POST https://totalpro.es/api/v1/auth/login -H "Content-Type: application/json" \
-d '{"email":"laura@empresa.es","password":"…","device":{"machine_id":"7c2e…","name":"PORTATIL-LAURA"}}'
curl https://totalpro.es/api/v1/me -H "Authorization: Bearer tp_at_…" # perfil, saldo para la IA y monedero
curl -X POST https://totalpro.es/api/v1/auth/refresh -d '{"refresh_token":"tp_rt_…"}' -H "Content-Type: application/json" Webhooks: totalpro.es → data.totalpro.es
user.created
Se ha registrado un usuariouser.updated
Cambian datos básicos del usuario (nombre, email, tipo)user.blocked
Un administrador bloquea al usuario: cerrar sesiones y bloquear sus agentesuser.unblocked
Un administrador desbloquea al usuarioentitlements.updated
Cambia lo que el usuario puede gastar en IA (recarga, regalo, límite mensual, ajuste del admin)ai.balance_low
Al usuario le queda poco saldo para la IA (menos de 1 € por defecto)ai.blocked
El usuario ya no puede usar la IA: sin saldo o límite mensual alcanzado. Parar las llamadas al modelosession.revoked
El usuario cierra sesión o se revoca un dispositivoticket.created
Se crea un ticket de soporte (también los abiertos desde el agente)ticket.quoted
El equipo envía (o cambia) el precio del ticket: el cliente ya puede pagarlo en pay_urlticket.status_changed
Un ticket cambia de estado (pagado, resuelto, devuelto, cancelado)ticket.offer_accepted
OBSOLETO: modelo de ofertas desactivado, ya no se emite
Eventos: data.totalpro.es → totalpro.es
POST /events · idempotente por id
machine.registered
El agente se ha registrado en un equipomachine.removed
Se ha desinstalado o desvinculado un equipoalert.created
El agente detecta un problema en un equipoconversation.escalated
La IA no lo resuelve y propone pasar a un técnicosupport.session_started
Un técnico inicia una sesión remotasupport.session_ended
Termina una sesión remota
Permisos de las claves de API
auth:verify— Verificar credenciales de usuarios y validar tokensusers:read— Leer usuarios, saldo para IA y consumousers:write— Cambiar el límite de gasto mensual o regalar crédito de IA a un usuariousage:write— Registrar (y cobrar) consumo de IAtickets:read— Leer ticketstickets:write— Crear tickets en nombre de un usuarioevents:write— Enviar eventos a totalpro.es
Errores
Formato application/problem+json. Programa contra title, que es estable; detail es un texto para personas.
{"type":"https://totalpro.es/desarrolladores#errores","title":"insufficient_scope","status":403,"detail":"La clave no tiene el permiso «usage:write».","required_scope":"usage:write"} Códigos habituales: invalid_credentials, password_not_set, user_blocked, too_many_attempts, invalid_token, token_reused, unauthorized, insufficient_scope, validation_error, not_found, payload_too_large.
Cargando la referencia… Si no aparece, descarga openapi.json.
¿Quieres integrar otra aplicación? Escríbenos a hola@totalpro.es y te daremos una clave de API con los permisos necesarios.