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étodoRutaScope
GET/api/v1/userprofile:read
GET/api/v1/workoutsworkouts:read
GET/api/v1/workouts/{id}workouts:read
GET/api/v1/workout-templatesworkouts:read
GET/api/v1/workout-templates/{id}workouts:read
GET/api/v1/splitssplits:read
GET/api/v1/splits/{id}splits:read
GET/api/v1/personal-recordsrecords:read
GET/api/v1/measurementsmeasurements:read
GET/api/v1/statsstats:read
GET/api/v1/trophiesstats:read
GET/api/v1/exercisesexercises:read
GET/api/v1/exercises/{id}exercises:read
GET/api/v1/equipmentexercises: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

GET/api/v1/userprofile:read

El perfil del usuario que autoriza: campos visibles, unidades preferidas y objetivo de entrenamiento.

Respuesta: Perfil de usuario

CampoTipoDescripción
idstringIdentificador estable del usuario; usa este como clave en tu almacenamiento.
displayNamestring | null
usernamestring | 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
heightnumber | nullEn la unidad indicada por heightUnit.
heightUnit"centimeters" | "feetInches"
goal"stronger" | "muscleMass" | "lean" | "generalFitness" | null
Ejemplo de respuesta
{
  "id": "8fKq2ZbXvNc…",
  "displayName": "Alex",
  "username": "alexlifts",
  "weightUnit": "pounds",
  "distanceUnit": "kilometers",
  "gender": "male",
  "height": 183,
  "heightUnit": "centimeters",
  "goal": "muscleMass"
}

Entrenamientos

GET/api/v1/workoutsworkouts:readpaginado

El historial de entrenamientos del usuario, del más reciente al más antiguo.

Parámetros

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30
startDatequerystring (ISO 8601)Solo entrenamientos que empiezan en este instante o después (inclusive).
endDatequerystring (ISO 8601)Solo entrenamientos que empiezan en este instante o antes (inclusive).

Respuesta: Resumen de entrenamiento (como items[] en el envoltorio de paginación)

CampoTipoDescripción
idstring (uuid)
namestring
notesstring | null
startDatestring (ISO 8601, UTC)
endDatestring (ISO 8601, UTC)
durationMinutesinteger
typestringOrigen del entrenamiento, p. ej. "custom", "external". Pueden aparecer valores nuevos.
volumenumberVolumen total levantado, en libras.
caloriesnumber
personalRecordCountintegerRécords conseguidos en este entrenamiento.
exerciseCountinteger
Ejemplo de respuesta
{
  "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
}
GET/api/v1/workouts/{id}workouts:read

Un entrenamiento completo: cada ejercicio con sus series registradas, en el orden en que se realizaron.

Parámetros

NombreEnTipoDescripción
idobligatoriopathstring (uuid)El id del entrenamiento del endpoint de listado.

Respuesta: Detalle de entrenamiento

CampoTipoDescripción
…todos los campos del resumen de entrenamientoIgual que el elemento del listado, sin exerciseCount.
exercises[].exerciseIdintegerId del ejercicio en el catálogo.
exercises[].exerciseNamestring
exercises[].supersetIdinteger | nullLos ejercicios que comparten un id se realizaron como superserie.
exercises[].sets[].orderintegerPosición dentro del ejercicio, empezando en 1.
exercises[].sets[].repsintegerRepeticiones realizadas.
exercises[].sets[].weightnumberLibras.
exercises[].sets[].durationinteger | nullSegundos, para series por tiempo.
exercises[].sets[].distanceinteger | nullMetros, para series por distancia.
exercises[].sets[].rpeinteger | nullÍndice de esfuerzo percibido (1–10).
exercises[].sets[].setType"normal" | "warmup" | "dropSet" | "failure"
Ejemplo de respuesta
{
  "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" }
      ]
    }
  ]
}
GET/api/v1/workout-templatesworkouts:readpaginado

Las 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

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30

Respuesta: Resumen de plantilla (como items[] en el envoltorio de paginación)

CampoTipoDescripción
idstring (uuid)
namestring
descriptionstring | null
exerciseCountinteger
exercisesarraySiempre vacío en las respuestas de listado; usa el endpoint de detalle.
GET/api/v1/workout-templates/{id}workouts:read

Una plantilla con sus ejercicios y series planificados.

Parámetros

NombreEnTipoDescripción
idobligatoriopathstring (uuid)El id de la plantilla del endpoint de listado.

Respuesta: Detalle de plantilla

CampoTipoDescripción
…todos los campos del resumen de plantilla
exercises[].exerciseIdinteger
exercises[].exerciseNamestring
exercises[].supersetIdinteger | null
exercises[].sets[].orderintegerPosición dentro del ejercicio, empezando en 1.
exercises[].sets[].repsinteger | nullRepeticiones planificadas.
exercises[].sets[].weightnumber | nullLibras.
exercises[].sets[].durationinteger | nullSegundos, para series por tiempo.
exercises[].sets[].distanceinteger | nullMetros, para series por distancia.
exercises[].sets[].rpeinteger | nullÍndice de esfuerzo percibido (1–10).
exercises[].sets[].setType"normal" | "warmup" | "dropSet" | "failure"
GET/api/v1/splitssplits:readpaginado

Los splits de entrenamiento personalizados del usuario, del más reciente al más antiguo.

Parámetros

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30

Respuesta: Resumen de split (como items[] en el envoltorio de paginación)

CampoTipoDescripción
idstring (uuid)
namestring | null
descriptionstring | null
sessionCountinteger
sessionsarraySiempre vacío en las respuestas de listado; usa el endpoint de detalle.
GET/api/v1/splits/{id}splits:read

Un split con sus sesiones en orden.

Parámetros

NombreEnTipoDescripción
idobligatoriopathstring (uuid)El id del split del endpoint de listado.

Respuesta: Detalle de split

CampoTipoDescripción
…todos los campos del resumen de split
sessions[].orderinteger
sessions[].type"muscles" | "workoutTemplate"
sessions[].musclesstring[]Músculos objetivo (camelCase, p. ej. "chest"); presente cuando type es muscles.
sessions[].workoutTemplateIdstring (uuid) | nullPlantilla vinculada; presente cuando type es workoutTemplate.

Récords y medidas

GET/api/v1/personal-recordsrecords:readpaginado

Los récords personales del usuario, del más reciente al más antiguo.

Parámetros

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30
exerciseIdqueryintegerFiltra a un solo ejercicio del catálogo.

Respuesta: Récord personal (como items[] en el envoltorio de paginación)

CampoTipoDescripción
exerciseIdinteger
exerciseNamestring
type"oneRepMax" | "weightMax" | "volumeMax" | "repetitionMax" | "durationMax" | "distanceMax"
repsinteger | null
weightnumber | nullLibras.
durationinteger | nullSegundos.
distanceinteger | nullMetros.
workoutIdstring (uuid) | nullEntrenamiento en el que se consiguió el récord, cuando se conoce.
achievedAtstring (ISO 8601, UTC)
Ejemplo de respuesta
{
  "exerciseId": 87,
  "exerciseName": "Deadlift",
  "type": "oneRepMax",
  "reps": 1,
  "weight": 405,
  "duration": null,
  "distance": null,
  "workoutId": "a1b2c3d4-…",
  "achievedAt": "2026-06-28T18:41:00Z"
}
GET/api/v1/measurementsmeasurements:readpaginado

Entradas de peso corporal y circunferencias, de la más reciente a la más antigua.

Parámetros

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30
startDatequerystring (ISO 8601)Solo entradas con fecha en este día o después (inclusive).
endDatequerystring (ISO 8601)Solo entradas con fecha en este día o antes (inclusive).

Respuesta: Medida (como items[] en el envoltorio de paginación)

CampoTipoDescripción
idstring (uuid)
datestring (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.
weightnumber | null
bodyFatnumber | nullPorcentaje.
neck / shoulder / chest / bicep / forearm / abdomen / waist / glutes / thigh / calfnumber | nullCircunferencias en el sistema de unidades de la entrada.

Estadísticas y trofeos

GET/api/v1/statsstats:read

Las estadísticas de entrenamiento principales del usuario en una sola llamada.

Respuesta: Estadísticas

CampoTipoDescripción
streak.currentintegerRacha actual, en semanas.
streak.longestintegerLa racha más larga hasta la fecha, en semanas.
streak.workoutsThisWeekinteger
totalXpinteger
trophyCountinteger
Ejemplo de respuesta
{
  "streak": { "current": 6, "longest": 14, "workoutsThisWeek": 3 },
  "totalXp": 12480,
  "trophyCount": 22
}
GET/api/v1/trophiesstats:readpaginado

Trofeos conseguidos, del más reciente al más antiguo.

Parámetros

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30

Respuesta: Trofeo (como items[] en el envoltorio de paginación)

CampoTipoDescripción
typestringTipo de trofeo, en camelCase, p. ej. "workout100", "streak5", "calendar15". Pueden aparecer valores nuevos.
exerciseIdinteger | nullPresente en los trofeos específicos de un ejercicio.
exerciseNamestring | null
achievedAtstring (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.

GET/api/v1/exercisesexercises:readpaginado

El catálogo de ejercicios más los ejercicios personalizados del usuario, en orden alfabético.

Parámetros

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30
searchquerystringCoincidencia por nombre sin distinguir mayúsculas (contiene). Máximo 200 caracteres.

Respuesta: Ejercicio (como items[] en el envoltorio de paginación)

CampoTipoDescripción
idintegerId estable de catálogo; lo referencian entrenamientos, récords y plantillas.
namestring
descriptionstring | null
categorystringEnum en camelCase, p. ej. "barbell", "dumbbell".
typestringEnum en camelCase, p. ej. "weight", "duration".
musclestringMúsculo principal, en camelCase, p. ej. "chest".
secondaryMusclesstring[]
equipmentstring[]Nombres de equipamiento utilizables para este ejercicio.
GET/api/v1/exercises/{id}exercises:read

Un ejercicio por su id de catálogo.

Parámetros

NombreEnTipoDescripción
idobligatoriopathintegerId del ejercicio en el catálogo.

Respuesta: Ejercicio

CampoTipoDescripción
…igual que el elemento del listado
GET/api/v1/equipmentexercises:readpaginado

El catálogo global de equipamiento, en orden alfabético.

Parámetros

NombreEnTipoDescripción
pagequeryintegerNúmero de página, empezando en 1. Por defecto: 1
pageSizequeryintegerElementos por página, de 1 a 100. Por defecto: 30

Respuesta: Equipamiento (como items[] en el envoltorio de paginación)

CampoTipoDescripción
idintegerId estable de catálogo.
namestring
subtitlestring | null
categorystringEnum en camelCase.
GET/api/v1/equipment/{id}exercises:read

Un artículo de equipamiento por su id de catálogo.

Parámetros

NombreEnTipoDescripción
idobligatoriopathintegerId del equipamiento en el catálogo.

Respuesta: Equipamiento

CampoTipoDescripció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.

GET/oauth/authorize

Pantalla de consentimiento alojada que inicia el flujo authorization-code. El usuario inicia sesión y aprueba; su navegador vuelve redirigido con ?code=gac_…&state=….

Parámetros

NombreEnTipoDescripción
client_idobligatorioquerystringEl client id de tu app.
redirect_uriobligatorioquerystringDebe coincidir exactamente con una redirect URI registrada.
scopequerystringScopes separados por espacios. Omítelo para todos los scopes permitidos a tu app.
statequerystringValor aleatorio que se devuelve tal cual; verifícalo a la vuelta (protección CSRF).
code_challengequerystringChallenge PKCE S256. Obligatorio para clientes públicos.
code_challenge_methodquerystringSolo se admite "S256".

Respuesta: Redirección

CampoTipoDescripción
codestringCódigo de autorización de un solo uso (gac_…), válido 10 minutos. Se devuelve en la redirección, no como JSON.
statestringSe devuelve tal cual desde la petición.
POST/oauth/token

Intercambia un código de autorización, o rota un token de refresco, por un par de tokens de acceso y refresco.

Parámetros

NombreEnTipoDescripción
grant_typeobligatorioformstring"authorization_code" o "refresh_token".
client_idobligatorioformstring
client_secretformstringSolo clientes confidenciales; los clientes públicos lo omiten (PKCE en su lugar).
codeformstringGrant authorization_code: el código gac_….
redirect_uriformstringGrant authorization_code: debe coincidir con la URI para la que se emitió el código.
code_verifierformstringGrant authorization_code: el verifier PKCE de tu challenge.
refresh_tokenformstringGrant refresh_token: el token grt_… (de un solo uso; se rota al completarse con éxito).

Respuesta: Respuesta de token

CampoTipoDescripción
access_tokenstringToken bearer (gat_…), válido 6 horas.
token_type"Bearer"
expires_inintegerSegundos hasta que expira el token de acceso.
refresh_tokenstringNuevo token de refresco rotatorio (grt_…), válido hasta 180 días.
scopestringScopes concedidos separados por espacios.
Ejemplo de respuesta
{
  "access_token": "gat_…",
  "token_type": "Bearer",
  "expires_in": 21600,
  "refresh_token": "grt_…",
  "scope": "workouts:read stats:read"
}
POST/oauth/revoke

Revoca un token (RFC 7009). Revocar un token de refresco revoca todo el grant. Devuelve 200 incluso para tokens desconocidos.

Parámetros

NombreEnTipoDescripción
tokenobligatorioformstringEl token de acceso o de refresco a revocar.
client_idobligatorioformstring
client_secretformstringSolo clientes confidenciales.

Respuesta: Vacío

CampoTipoDescripción
(sin cuerpo)200 si tiene éxito; 401 invalid_client si las credenciales del cliente no son válidas.