🔗 WHOOP OAuth: crear la app
Para que el coach lea tu WHOOP, primero creas un app de desarrollador en el portal de WHOOP. Esta app le da una identidad a HealthOS (client id + secret) y dice exactamente adónde debe llevarte WHOOP después de que autorices. Tres decisiones lo definen todo: el redirect URI, los alcances e o offline.
🟢 ¿Nuevo por aquí? Qué es OAuth
OAuth es la forma estándar de «dar permiso sin entregar la contraseña». En vez de escribir tu usuario y contraseña de WHOOP en HealthOS, haces clic en Autorizar en el sitio de WHOOP y esta devuelve un token — una clave temporal que solo sirve para leer lo que autorizaste. El redirect URI es la dirección a la que WHOOP te devuelve con este token.
En el portal para desarrolladores, tres pasos:
Crear la app
En el portal para desarrolladores de WHOOP, crea una app. Recibes un WHOOP_CLIENT_ID y un WHOOP_CLIENT_SECRET — van a ~/.env.
Registrar el redirect URI
Pega exactamente https://<seu-host>/whoop/callback. Debe coincidir carácter por carácter con lo que usa tu callback; si no, WHOOP rechazará la autorización.
Habilitar los permisos
Marca read:recovery read:sleep read:cycles offline. Los tres read: liberan los datos; el offline libera el refresh token (sync sin que estés presente).
📋 Pega en el panel · objetivo: los dos campos exactos de la app WHOOP
Redirect URI: https://<seu-host>/whoop/callback
Scopes: read:recovery read:sleep read:cycles offline
Cómo verificar: el panel guarda sin quejarse y el offline aparece marcado. Sin el offline tendrías que volver a autorizar todos los días; la sincronización automática no se ejecutaría.
📊 Cómo leer: de izquierda a derecha está el recorrido del permiso hasta el dato: la app autoriza en el callback, el callback guarda el token, la sincronización usa el token y escribe en vitals. La línea de vuelta discontinua es la rotación del token (gotcha 1 del tema 4): en cada ejecución, el token se cambia por el siguiente.
Conceptos clave
Le da id + secret a HealthOS.
.../whoop/callback, exacto.
recovery + sleep + cycles.
Libera el refresh token.
✅ Autorización de una sola vez
Autorizas a WHOOP una sola vez. Con esta autorización, tu callback recibe el código, lo intercambia por tokens y guarda el WHOOP_REFRESH_TOKEN en el ~/.env. A partir de ahí, la sincronización diaria funciona sola con ese refresh token: no necesitas volver a abrir el navegador.
Haces clic en «Autorizar»
Abre la pantalla de consentimiento de WHOOP con los permisos que marcaste. Aceptas.
WHOOP redirige
Te lleva a https://<seu-host>/whoop/callback con un código de un solo uso.
El callback guarda el token
El callback intercambia el código por tokens y escribe el WHOOP_REFRESH_TOKEN en el ~/.env. Listo: el one-time terminó.
▶ Ejecútalo tú mismo · objetivo: comprobar que se guardó el refresh token
grep WHOOP_REFRESH_TOKEN ~/.env
Cómo verificar: aparece una línea WHOOP_REFRESH_TOKEN=... con un valor largo (no vacío). Si viene vacío, la autorización no se completó: vuelve a hacer el paso de "Autorizar".
💾 Lo que quedó en el ~/.env
Después del one-time, tu ~/.env tiene los tres: WHOOP_CLIENT_ID, WHOOP_CLIENT_SECRET (de la app, en el tema 1) y ahora el WHOOP_REFRESH_TOKEN (guardado por el callback, rotado en cada sincronización). Este archivo está en tu directorio personal y nunca va a git.
Conceptos clave
Autorízalo una sola vez.
Cambia código por tokens.
Guardado en ~/.env.
Después, el sync se encarga solo.
🔄 whoop-sync.py
O agent/scripts/whoop-sync.py es el corazón de la conexión. Cada vez que se ejecuta, obtiene la recuperación y el sueño de WHOOP y guarda cuatro campos en la tabla vitals: recovery_pct, hrv_ms, resting_hr, sleep_hours. Es determinista (mismo día, mismo resultado) y idempotente (volver a ejecutarlo no crea una línea duplicada: actualiza la de ese día).
🟢 ¿Nuevo por aquí?
- Determinista — no hay IA ni aleatoriedad de por medio: con los mismos datos del día en WHOOP, la salida siempre es la misma. Puedes confiar en ella y volver a ejecutarla.
- Idempotente — ejecutarlo 1× o 5× deja la base de datos en el mismo estado: una fila por día en
vitals, no cinco. Por eso es seguro programar repeticiones.
🗄️ Los 4 campos que caen en vitals
% de recuperación de la noche.
Variabilidad cardíaca (ms).
Frecuencia en reposo.
Horas de sueño de la noche.
▶ Ejecútalo tú mismo · objetivo: sincronizar ahora y ver los 4 campos en vitals
python3 agent/scripts/whoop-sync.py
python3 agent/scripts/db.py select vitals
Cómo verificar: la línea de hoy en vitals trae recovery_pct, hrv_ms, resting_hr e sleep_hours completados. Vuelve a ejecutar los dos comandos: sigue una fila para el día (idempotente), no dos.
Conceptos clave
La noche que WHOOP puntuó.
4 campos por día.
El mismo día, el mismo resultado.
Volver a ejecutar no duplica.
🛡️ Los 2 gotchas de producción
Dos detalles hacen fallar la sincronización si no los resuelves, y ambos solo aparecen en producción, después de que la primera sync ya haya funcionado. El whoop-sync.py ya se encarga de ambos; entiende por qué para no tropezar al reescribirlo.
✗ Si lo ignoras
- ✗(a) Reutilizar el mismo refresh token → en la 2.ª ejecución, WHOOP responde
invalid_granty la sincronización falla. - ✗(b) Usar el user-agent predeterminado de Python → Cloudflare de WHOOP te bloquea con error 1010.
✓ Qué hace la sincronización
- ✓(a) En cada run, guarda el nuevo refresh token que WHOOP devuelve sobre el anterior (rotación).
- ✓(b) Envía un user-agent de browser en las solicitudes, así que Cloudflare las deja pasar.
⚠️ Cómo aparece cada fallo
- •Gotcha (a) — rotación: el refresh token es de uso único. Cuando lo usas, WHOOP devuelve uno nuevo e invalida el anterior. Si no guardas el nuevo, la siguiente ejecución usa un token ya consumido →
invalid_grant. Verifica ejecutando el sync dos veces: la 2.ª aún debería tener éxito. - •Gotcha (b) — user-agent: Cloudflare, delante de la API, bloquea a los clientes «robóticos». El user-agent predeterminado del
requests/urllibentrega el juego → 1010. Enviar un header de browser lo resuelve.
Conceptos clave
Guardar el token nuevo en cada ejecución.
Síntoma de token no rotado.
Simular un navegador supera Cloudflare.
Cloudflare bloqueó al cliente.
⏰ Programar la sincronización
WHOOP no puntúa tu noche a una hora fija: a veces a las 7h, a veces más tarde. Por eso la sincronización se ejecuta tres veces por la mañana (7h, 10h, 13h): alguna de estas ejecuciones detecta la recuperación en cuanto esté lista. Como la sincronización es idempotente, repetirla es gratis y seguro.
📊 Cómo leer: a la izquierda, el cron activa cuatro cosas por la mañana: tres sincronizaciones (azul: 7h/10h/13h) que escriben en vitals, e o /checkin (ámbar, tema 6) que lee la recuperación y envía el análisis a Telegram. Las tres horas garantizan detectar la recovery en cuanto WHOOP le asigne una puntuación.
▶ Ejecútalo tú mismo · objetivo: programar la sincronización 3× por la mañana (Linux/cron)
# abra o crontab: crontab -e
# sync às 7h, 10h e 13h (idempotente, repetir é seguro)
0 7,10,13 * * * cd ~/<sua-pasta>/agent && /usr/bin/python3 scripts/whoop-sync.py >> ~/whoop-sync.log 2>&1
macOS: en lugar del cron, usa agent/setup/whoop-sync.plist.example (launchd). Cómo verificar: mañana por la mañana la tabla vitals gana la línea del día y ~/whoop-sync.log muestra ejecuciones sin errores (sin invalid_grant, sin 1010).
Conceptos clave
Obtén la recovery cuando esté disponible.
Una línea programa los 3 horarios.
.plist.example listo en el repo.
La idempotencia protege.
🌅 Programar el check-in de la mañana
El sync solo llena la tabla; quien te entrega la revisión es el check-in de la mañana. A diferencia del sync (que es solo un script que lee la API), el check-in activa un turno del agente — una llamada al LLM que lee la recuperación de la noche y escribe la revisión en Telegram. Por eso se programa por separado: envías el prompt /checkin cada mañana mediante tu scheduler.
💡 Sincronización ≠ registro
O sincronización es mecánico y barato: lee la WHOOP y guarda 4 números. El check-in es un turno del agente (cuesta una llamada al LLM): lee estos números + el resto de tu snapshot y conversación contigo. Programa el sync para que se ejecute antes del check-in, para que la revisión ya encuentre la recuperación del día lista.
▶ Ejecútalo tú mismo · objetivo: activar el /checkin cada mañana (un turno del agente)
# crontab -e — dispara /checkin às 7h05 (logo após o 1º sync)
5 7 * * * cd ~/<sua-pasta>/agent && /usr/bin/python3 <seu-disparador> "/checkin" >> ~/checkin.log 2>&1
Atajo: las plataformas de agentes con scheduler integrado (p. ej., ClaudeClaw) activan el /checkin directamente, sin cron. Cómo verificar: a las 7 h recibes en Telegram la revisión de la mañana abriendo con la recuperación de la noche y vinculándola con las decisiones de ayer.
🔁 El orden de la mañana
1) 07:00 el sync escribe la recovery en vitals. 2) 07:05 o /checkin activa el agente, que lee esa recuperación y te envía la revisión. Las sincronizaciones de las 10h y 13h son la red de seguridad por si WHOOP puntúa la noche más tarde.
Conceptos clave
El prompt de la revisión de la mañana.
Una llamada a un LLM, no un script.
Recovery en la mesa cuando se activa.
Cron o el de tu agente.
🔁 Otros wearables
WHOOP es solo el ejemplo resuelto. El mismo patrón — OAuth una vez + sync diario que escribe en vitals — sirve para Oura, Garmin, Fitbit o incluso para un registro manual. Al coach y al dashboard no les importa ni saben de dónde viene el número: trabajan con lo que llegue a la tabla vitals.
✓ Reutiliza el patrón
- ✓Oura / Garmin / Fitbit — cambiar la app OAuth y el mapeo JSON→
vitals; el resto queda igual. - ✓Registro manual — un mensaje en Telegram ("dormí 7 h, buena recuperación") ya se convierte en una fila en
vitals. - ✓O
/checkin, el snapshot y el dashboard siguen idénticos.
✗ Lo que no cambia
- ✗No reescribas el coach según la fuente — lee
vitals, no la API del wearable. - ✗No te saltes los 2 gotchas: cualquiera OAuth tiene rotación de token y protección antibot al frente.
- ✗No necesitas WHOOP para empezar — el curso funciona con registro manual.
✅ Autoevaluación (opcional): por qué el whoop-sync.py ¿necesitaba enviar un user-agent de browser?
Conceptos clave
OAuth + sync diario.
El coach lee la tabla, no la API.
El mensaje se convierte en una fila.
Rotación + antibot en cada fuente.
📋 Resumen del módulo
.../whoop/callback exacto y marca read:recovery read:sleep read:cycles offline.WHOOP_REFRESH_TOKEN en el ~/.env.invalid_grant) y envía un user-agent de navegador (si no, Cloudflare 1010)./checkin de la mañana; el patrón sirve para cualquier wearable.Próximo módulo:
Ruta 3 — Cómo usar: la rutina diaria, comida y entrenamiento por foto, memoria/patrones y el dashboard. El paso a paso terminó; ahora toca vivir el coach en el día a día.