Referencia
Endpoints
Todo vive bajo https://api.gravl.ai y devuelve JSON. Los datos de usuario se identifican con GUIDs estables; los recursos de catálogo, con ids enteros. Los valores enum son cadenas en camelCase: trata los valores desconocidos como compatibles hacia delante.
Todos los endpoints
| Método | Ruta | Scope |
|---|---|---|
| GET | /api/v1/user | profile:read |
| GET | /api/v1/workouts | workouts:read |
| GET | /api/v1/workouts/{id} | workouts:read |
| GET | /api/v1/workout-templates | workouts:read |
| GET | /api/v1/workout-templates/{id} | workouts:read |
| GET | /api/v1/splits | splits:read |
| GET | /api/v1/splits/{id} | splits:read |
| GET | /api/v1/personal-records | records:read |
| GET | /api/v1/measurements | measurements:read |
| GET | /api/v1/stats | stats:read |
| GET | /api/v1/trophies | stats:read |
| GET | /api/v1/exercises | exercises:read |
| GET | /api/v1/exercises/{id} | exercises:read |
| GET | /api/v1/equipment | exercises:read |
| GET | /api/v1/equipment/{id} | exercises:read |
| GET | /oauth/authorize | — |
| POST | /oauth/token | — |
| POST | /oauth/revoke | — |
El envoltorio de paginación
Los endpoints marcados abajo como paginado envuelven sus elementos en un envoltorio estándar:
{
"items": [ … ], // the page of results
"pageNumber": 2,
"totalPages": 9,
"totalCount": 174,
"hasPreviousPage": true,
"hasNextPage": true
}Cada endpoint exige su scope; si falta, devuelve 403 con problem details. Los errores y los rate limits están documentados en convenciones.
Usuario
/api/v1/userprofile:readEl perfil del usuario que autoriza: campos visibles, unidades preferidas y objetivo de entrenamiento.
Respuesta: Perfil de usuario
| Campo | Tipo | Descripción |
|---|---|---|
| id | string | Identificador estable del usuario; usa este como clave en tu almacenamiento. |
| displayName | string | null | |
| username | string | null | |
| weightUnit | "kilograms" | "pounds" | Unidad de visualización preferida; los pesos de la API siempre están en libras. |
| distanceUnit | "kilometers" | "miles" | |
| gender | "male" | "female" | "other" | null | |
| height | number | null | En la unidad indicada por heightUnit. |
| heightUnit | "centimeters" | "feetInches" | |
| goal | "stronger" | "muscleMass" | "lean" | "generalFitness" | null |
{
"id": "8fKq2ZbXvNc…",
"displayName": "Alex",
"username": "alexlifts",
"weightUnit": "pounds",
"distanceUnit": "kilometers",
"gender": "male",
"height": 183,
"heightUnit": "centimeters",
"goal": "muscleMass"
}Entrenamientos
/api/v1/workoutsworkouts:readpaginadoEl historial de entrenamientos del usuario, del más reciente al más antiguo.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
startDate | query | string (ISO 8601) | Solo entrenamientos que empiezan en este instante o después (inclusive). |
endDate | query | string (ISO 8601) | Solo entrenamientos que empiezan en este instante o antes (inclusive). |
Respuesta: Resumen de entrenamiento (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| id | string (uuid) | |
| name | string | |
| notes | string | null | |
| startDate | string (ISO 8601, UTC) | |
| endDate | string (ISO 8601, UTC) | |
| durationMinutes | integer | |
| type | string | Origen del entrenamiento, p. ej. "custom", "external". Pueden aparecer valores nuevos. |
| volume | number | Volumen total levantado, en libras. |
| calories | number | |
| personalRecordCount | integer | Récords conseguidos en este entrenamiento. |
| exerciseCount | integer |
{
"items": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Push Day",
"notes": null,
"startDate": "2026-07-12T17:03:00Z",
"endDate": "2026-07-12T18:05:00Z",
"durationMinutes": 62,
"type": "custom",
"volume": 24150,
"calories": 410,
"personalRecordCount": 1,
"exerciseCount": 6
}
],
"pageNumber": 1,
"totalPages": 87,
"totalCount": 174,
"hasPreviousPage": false,
"hasNextPage": true
}/api/v1/workouts/{id}workouts:readUn entrenamiento completo: cada ejercicio con sus series registradas, en el orden en que se realizaron.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
idobligatorio | path | string (uuid) | El id del entrenamiento del endpoint de listado. |
Respuesta: Detalle de entrenamiento
| Campo | Tipo | Descripción |
|---|---|---|
| …todos los campos del resumen de entrenamiento | Igual que el elemento del listado, sin exerciseCount. | |
| exercises[].exerciseId | integer | Id del ejercicio en el catálogo. |
| exercises[].exerciseName | string | |
| exercises[].supersetId | integer | null | Los ejercicios que comparten un id se realizaron como superserie. |
| exercises[].sets[].order | integer | Posición dentro del ejercicio, empezando en 1. |
| exercises[].sets[].reps | integer | Repeticiones realizadas. |
| exercises[].sets[].weight | number | Libras. |
| exercises[].sets[].duration | integer | null | Segundos, para series por tiempo. |
| exercises[].sets[].distance | integer | null | Metros, para series por distancia. |
| exercises[].sets[].rpe | integer | null | Índice de esfuerzo percibido (1–10). |
| exercises[].sets[].setType | "normal" | "warmup" | "dropSet" | "failure" |
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Push Day",
"startDate": "2026-07-12T17:03:00Z",
"endDate": "2026-07-12T18:05:00Z",
"durationMinutes": 62,
"type": "custom",
"volume": 24150,
"calories": 410,
"personalRecordCount": 1,
"exercises": [
{
"exerciseId": 42,
"exerciseName": "Bench Press",
"supersetId": null,
"sets": [
{ "order": 1, "reps": 10, "weight": 155, "duration": null,
"distance": null, "rpe": 7, "setType": "normal" },
{ "order": 2, "reps": 8, "weight": 175, "duration": null,
"distance": null, "rpe": 9, "setType": "normal" }
]
}
]
}/api/v1/workout-templatesworkouts:readpaginadoLas plantillas de entrenamiento guardadas del usuario, de la más reciente a la más antigua. Las series son valores planificados, no registros.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
Respuesta: Resumen de plantilla (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| id | string (uuid) | |
| name | string | |
| description | string | null | |
| exerciseCount | integer | |
| exercises | array | Siempre vacío en las respuestas de listado; usa el endpoint de detalle. |
/api/v1/workout-templates/{id}workouts:readUna plantilla con sus ejercicios y series planificados.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
idobligatorio | path | string (uuid) | El id de la plantilla del endpoint de listado. |
Respuesta: Detalle de plantilla
| Campo | Tipo | Descripción |
|---|---|---|
| …todos los campos del resumen de plantilla | ||
| exercises[].exerciseId | integer | |
| exercises[].exerciseName | string | |
| exercises[].supersetId | integer | null | |
| exercises[].sets[].order | integer | Posición dentro del ejercicio, empezando en 1. |
| exercises[].sets[].reps | integer | null | Repeticiones planificadas. |
| exercises[].sets[].weight | number | null | Libras. |
| exercises[].sets[].duration | integer | null | Segundos, para series por tiempo. |
| exercises[].sets[].distance | integer | null | Metros, para series por distancia. |
| exercises[].sets[].rpe | integer | null | Índice de esfuerzo percibido (1–10). |
| exercises[].sets[].setType | "normal" | "warmup" | "dropSet" | "failure" |
/api/v1/splitssplits:readpaginadoLos splits de entrenamiento personalizados del usuario, del más reciente al más antiguo.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
Respuesta: Resumen de split (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| id | string (uuid) | |
| name | string | null | |
| description | string | null | |
| sessionCount | integer | |
| sessions | array | Siempre vacío en las respuestas de listado; usa el endpoint de detalle. |
/api/v1/splits/{id}splits:readUn split con sus sesiones en orden.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
idobligatorio | path | string (uuid) | El id del split del endpoint de listado. |
Respuesta: Detalle de split
| Campo | Tipo | Descripción |
|---|---|---|
| …todos los campos del resumen de split | ||
| sessions[].order | integer | |
| sessions[].type | "muscles" | "workoutTemplate" | |
| sessions[].muscles | string[] | Músculos objetivo (camelCase, p. ej. "chest"); presente cuando type es muscles. |
| sessions[].workoutTemplateId | string (uuid) | null | Plantilla vinculada; presente cuando type es workoutTemplate. |
Récords y medidas
/api/v1/personal-recordsrecords:readpaginadoLos récords personales del usuario, del más reciente al más antiguo.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
exerciseId | query | integer | Filtra a un solo ejercicio del catálogo. |
Respuesta: Récord personal (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| exerciseId | integer | |
| exerciseName | string | |
| type | "oneRepMax" | "weightMax" | "volumeMax" | "repetitionMax" | "durationMax" | "distanceMax" | |
| reps | integer | null | |
| weight | number | null | Libras. |
| duration | integer | null | Segundos. |
| distance | integer | null | Metros. |
| workoutId | string (uuid) | null | Entrenamiento en el que se consiguió el récord, cuando se conoce. |
| achievedAt | string (ISO 8601, UTC) |
{
"exerciseId": 87,
"exerciseName": "Deadlift",
"type": "oneRepMax",
"reps": 1,
"weight": 405,
"duration": null,
"distance": null,
"workoutId": "a1b2c3d4-…",
"achievedAt": "2026-06-28T18:41:00Z"
}/api/v1/measurementsmeasurements:readpaginadoEntradas de peso corporal y circunferencias, de la más reciente a la más antigua.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
startDate | query | string (ISO 8601) | Solo entradas con fecha en este día o después (inclusive). |
endDate | query | string (ISO 8601) | Solo entradas con fecha en este día o antes (inclusive). |
Respuesta: Medida (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| id | string (uuid) | |
| date | string (ISO 8601 date) | |
| weightUnit | "kilograms" | "pounds" | Sistema de unidades de esta entrada: las circunferencias van en pulgadas con libras y en centímetros con kilogramos. |
| weight | number | null | |
| bodyFat | number | null | Porcentaje. |
| neck / shoulder / chest / bicep / forearm / abdomen / waist / glutes / thigh / calf | number | null | Circunferencias en el sistema de unidades de la entrada. |
Estadísticas y trofeos
/api/v1/statsstats:readLas estadísticas de entrenamiento principales del usuario en una sola llamada.
Respuesta: Estadísticas
| Campo | Tipo | Descripción |
|---|---|---|
| streak.current | integer | Racha actual, en semanas. |
| streak.longest | integer | La racha más larga hasta la fecha, en semanas. |
| streak.workoutsThisWeek | integer | |
| totalXp | integer | |
| trophyCount | integer |
{
"streak": { "current": 6, "longest": 14, "workoutsThisWeek": 3 },
"totalXp": 12480,
"trophyCount": 22
}/api/v1/trophiesstats:readpaginadoTrofeos conseguidos, del más reciente al más antiguo.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
Respuesta: Trofeo (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| type | string | Tipo de trofeo, en camelCase, p. ej. "workout100", "streak5", "calendar15". Pueden aparecer valores nuevos. |
| exerciseId | integer | null | Presente en los trofeos específicos de un ejercicio. |
| exerciseName | string | null | |
| achievedAt | string (ISO 8601, UTC) |
Catálogo de ejercicios y equipamiento
Datos de referencia globales, identificados con los mismos ids enteros estables que usa la app de Gravl. Los listados de ejercicios también incluyen los ejercicios personalizados del propio usuario que autoriza, nunca los de otros usuarios.
/api/v1/exercisesexercises:readpaginadoEl catálogo de ejercicios más los ejercicios personalizados del usuario, en orden alfabético.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
search | query | string | Coincidencia por nombre sin distinguir mayúsculas (contiene). Máximo 200 caracteres. |
Respuesta: Ejercicio (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| id | integer | Id estable de catálogo; lo referencian entrenamientos, récords y plantillas. |
| name | string | |
| description | string | null | |
| category | string | Enum en camelCase, p. ej. "barbell", "dumbbell". |
| type | string | Enum en camelCase, p. ej. "weight", "duration". |
| muscle | string | Músculo principal, en camelCase, p. ej. "chest". |
| secondaryMuscles | string[] | |
| equipment | string[] | Nombres de equipamiento utilizables para este ejercicio. |
/api/v1/exercises/{id}exercises:readUn ejercicio por su id de catálogo.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
idobligatorio | path | integer | Id del ejercicio en el catálogo. |
Respuesta: Ejercicio
| Campo | Tipo | Descripción |
|---|---|---|
| …igual que el elemento del listado |
/api/v1/equipmentexercises:readpaginadoEl catálogo global de equipamiento, en orden alfabético.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
page | query | integer | Número de página, empezando en 1. Por defecto: 1 |
pageSize | query | integer | Elementos por página, de 1 a 100. Por defecto: 30 |
Respuesta: Equipamiento (como items[] en el envoltorio de paginación)
| Campo | Tipo | Descripción |
|---|---|---|
| id | integer | Id estable de catálogo. |
| name | string | |
| subtitle | string | null | |
| category | string | Enum en camelCase. |
/api/v1/equipment/{id}exercises:readUn artículo de equipamiento por su id de catálogo.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
idobligatorio | path | integer | Id del equipamiento en el catálogo. |
Respuesta: Equipamiento
| Campo | Tipo | Descripción |
|---|---|---|
| …igual que el elemento del listado |
OAuth
Los endpoints de token van codificados como formulario (application/x-www-form-urlencoded) y siguen las RFC 6749/7009. El flujo completo está documentado en la página de apps OAuth.
/oauth/tokenIntercambia un código de autorización, o rota un token de refresco, por un par de tokens de acceso y refresco.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
grant_typeobligatorio | form | string | "authorization_code" o "refresh_token". |
client_idobligatorio | form | string | |
client_secret | form | string | Solo clientes confidenciales; los clientes públicos lo omiten (PKCE en su lugar). |
code | form | string | Grant authorization_code: el código gac_…. |
redirect_uri | form | string | Grant authorization_code: debe coincidir con la URI para la que se emitió el código. |
code_verifier | form | string | Grant authorization_code: el verifier PKCE de tu challenge. |
refresh_token | form | string | Grant refresh_token: el token grt_… (de un solo uso; se rota al completarse con éxito). |
Respuesta: Respuesta de token
| Campo | Tipo | Descripción |
|---|---|---|
| access_token | string | Token bearer (gat_…), válido 6 horas. |
| token_type | "Bearer" | |
| expires_in | integer | Segundos hasta que expira el token de acceso. |
| refresh_token | string | Nuevo token de refresco rotatorio (grt_…), válido hasta 180 días. |
| scope | string | Scopes concedidos separados por espacios. |
{
"access_token": "gat_…",
"token_type": "Bearer",
"expires_in": 21600,
"refresh_token": "grt_…",
"scope": "workouts:read stats:read"
}/oauth/revokeRevoca un token (RFC 7009). Revocar un token de refresco revoca todo el grant. Devuelve 200 incluso para tokens desconocidos.
Parámetros
| Nombre | En | Tipo | Descripción |
|---|---|---|---|
tokenobligatorio | form | string | El token de acceso o de refresco a revocar. |
client_idobligatorio | form | string | |
client_secret | form | string | Solo clientes confidenciales. |
Respuesta: Vacío
| Campo | Tipo | Descripción |
|---|---|---|
| (sin cuerpo) | 200 si tiene éxito; 401 invalid_client si las credenciales del cliente no son válidas. |