Reference

Endpoints

Everything lives under https://api.gravl.ai and returns JSON. User data is identified by stable GUIDs; catalog resources by integer ids. Enum values are camelCase strings — treat unknown values as forward-compatible.

All endpoints

MethodPathScope
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

The pagination envelope

Endpoints marked paginated below wrap their items in one standard envelope:

{
  "items": [ … ],           // the page of results
  "pageNumber": 2,
  "totalPages": 9,
  "totalCount": 174,
  "hasPreviousPage": true,
  "hasNextPage": true
}

Every endpoint requires its scope — a missing scope returns 403 with problem details. Errors and rate limits are documented under conventions.

User

GET/api/v1/userprofile:read

The authorizing user’s profile: display fields, preferred units and training goal.

Response — User profile

FieldTypeDescription
idstringStable user identifier — key your storage on this.
displayNamestring | null
usernamestring | null
weightUnit"kilograms" | "pounds"Preferred display unit — API weights are always pounds.
distanceUnit"kilometers" | "miles"
gender"male" | "female" | "other" | null
heightnumber | nullIn the unit given by heightUnit.
heightUnit"centimeters" | "feetInches"
goal"stronger" | "muscleMass" | "lean" | "generalFitness" | null
Example response
{
  "id": "8fKq2ZbXvNc…",
  "displayName": "Alex",
  "username": "alexlifts",
  "weightUnit": "pounds",
  "distanceUnit": "kilometers",
  "gender": "male",
  "height": 183,
  "heightUnit": "centimeters",
  "goal": "muscleMass"
}

Workouts

GET/api/v1/workoutsworkouts:readpaginated

The user’s workout history, newest first.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30
startDatequerystring (ISO 8601)Only workouts starting on/after this instant (inclusive).
endDatequerystring (ISO 8601)Only workouts starting on/before this instant (inclusive).

Response — Workout summary (as items[] in the pagination envelope)

FieldTypeDescription
idstring (uuid)
namestring
notesstring | null
startDatestring (ISO 8601, UTC)
endDatestring (ISO 8601, UTC)
durationMinutesinteger
typestringWorkout origin, e.g. "custom", "external". New values may appear.
volumenumberTotal volume lifted, pounds.
caloriesnumber
personalRecordCountintegerPRs set in this workout.
exerciseCountinteger
Example response
{
  "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

One workout in full: every exercise with its logged sets, in performed order.

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)The workout id from the list endpoint.

Response — Workout detail

FieldTypeDescription
…all workout summary fieldsSame as the list item, minus exerciseCount.
exercises[].exerciseIdintegerCatalog exercise id.
exercises[].exerciseNamestring
exercises[].supersetIdinteger | nullExercises sharing an id were performed as a superset.
exercises[].sets[].orderinteger1-based position within the exercise.
exercises[].sets[].repsintegerRepetitions performed.
exercises[].sets[].weightnumberPounds.
exercises[].sets[].durationinteger | nullSeconds, for timed sets.
exercises[].sets[].distanceinteger | nullMeters, for distance sets.
exercises[].sets[].rpeinteger | nullRate of perceived exertion (1–10).
exercises[].sets[].setType"normal" | "warmup" | "dropSet" | "failure"
Example response
{
  "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:readpaginated

The user’s saved workout templates, newest first. Sets are planned values, not logs.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30

Response — Template summary (as items[] in the pagination envelope)

FieldTypeDescription
idstring (uuid)
namestring
descriptionstring | null
exerciseCountinteger
exercisesarrayAlways empty in list responses — use the detail endpoint.
GET/api/v1/workout-templates/{id}workouts:read

One template with its planned exercises and sets.

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)The template id from the list endpoint.

Response — Template detail

FieldTypeDescription
…all template summary fields
exercises[].exerciseIdinteger
exercises[].exerciseNamestring
exercises[].supersetIdinteger | null
exercises[].sets[].orderinteger1-based position within the exercise.
exercises[].sets[].repsinteger | nullPlanned repetitions.
exercises[].sets[].weightnumber | nullPounds.
exercises[].sets[].durationinteger | nullSeconds, for timed sets.
exercises[].sets[].distanceinteger | nullMeters, for distance sets.
exercises[].sets[].rpeinteger | nullRate of perceived exertion (1–10).
exercises[].sets[].setType"normal" | "warmup" | "dropSet" | "failure"
GET/api/v1/splitssplits:readpaginated

The user’s custom training splits, newest first.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30

Response — Split summary (as items[] in the pagination envelope)

FieldTypeDescription
idstring (uuid)
namestring | null
descriptionstring | null
sessionCountinteger
sessionsarrayAlways empty in list responses — use the detail endpoint.
GET/api/v1/splits/{id}splits:read

One split with its sessions in order.

Parameters

NameInTypeDescription
idrequiredpathstring (uuid)The split id from the list endpoint.

Response — Split detail

FieldTypeDescription
…all split summary fields
sessions[].orderinteger
sessions[].type"muscles" | "workoutTemplate"
sessions[].musclesstring[]Target muscles (camelCase, e.g. "chest"); set when type is muscles.
sessions[].workoutTemplateIdstring (uuid) | nullLinked template; set when type is workoutTemplate.

Records & measurements

GET/api/v1/personal-recordsrecords:readpaginated

The user’s personal records, newest first.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30
exerciseIdqueryintegerFilter to one catalog exercise.

Response — Personal record (as items[] in the pagination envelope)

FieldTypeDescription
exerciseIdinteger
exerciseNamestring
type"oneRepMax" | "weightMax" | "volumeMax" | "repetitionMax" | "durationMax" | "distanceMax"
repsinteger | null
weightnumber | nullPounds.
durationinteger | nullSeconds.
distanceinteger | nullMeters.
workoutIdstring (uuid) | nullWorkout the record was set in, when known.
achievedAtstring (ISO 8601, UTC)
Example response
{
  "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:readpaginated

Body weight and circumference entries, newest first.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30
startDatequerystring (ISO 8601)Only entries dated on/after this (inclusive).
endDatequerystring (ISO 8601)Only entries dated on/before this (inclusive).

Response — Measurement (as items[] in the pagination envelope)

FieldTypeDescription
idstring (uuid)
datestring (ISO 8601 date)
weightUnit"kilograms" | "pounds"Unit system of this entry: circumferences are inches with pounds, centimeters with kilograms.
weightnumber | null
bodyFatnumber | nullPercent.
neck / shoulder / chest / bicep / forearm / abdomen / waist / glutes / thigh / calfnumber | nullCircumferences in the entry’s unit system.

Stats & trophies

GET/api/v1/statsstats:read

The user’s headline training stats in one call.

Response — Stats

FieldTypeDescription
streak.currentintegerCurrent streak, weeks.
streak.longestintegerLongest streak ever, weeks.
streak.workoutsThisWeekinteger
totalXpinteger
trophyCountinteger
Example response
{
  "streak": { "current": 6, "longest": 14, "workoutsThisWeek": 3 },
  "totalXp": 12480,
  "trophyCount": 22
}
GET/api/v1/trophiesstats:readpaginated

Earned trophies, newest first.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30

Response — Trophy (as items[] in the pagination envelope)

FieldTypeDescription
typestringTrophy kind, camelCase — e.g. "workout100", "streak5", "calendar15". New values may appear.
exerciseIdinteger | nullSet for exercise-specific trophies.
exerciseNamestring | null
achievedAtstring (ISO 8601, UTC)

Exercise & equipment catalog

Global reference data, identified by the same stable integer ids the Gravl app uses. Exercise listings also include the authorizing user’s own custom exercises — never other users’.

GET/api/v1/exercisesexercises:readpaginated

The exercise catalog plus the user’s custom exercises, alphabetical.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30
searchquerystringCase-insensitive name match (contains). Max 200 chars.

Response — Exercise (as items[] in the pagination envelope)

FieldTypeDescription
idintegerStable catalog id — referenced by workouts, records and templates.
namestring
descriptionstring | null
categorystringcamelCase enum, e.g. "barbell", "dumbbell".
typestringcamelCase enum, e.g. "weight", "duration".
musclestringPrimary muscle, camelCase, e.g. "chest".
secondaryMusclesstring[]
equipmentstring[]Equipment names usable for this exercise.
GET/api/v1/exercises/{id}exercises:read

One exercise by catalog id.

Parameters

NameInTypeDescription
idrequiredpathintegerCatalog exercise id.

Response — Exercise

FieldTypeDescription
…same as the list item
GET/api/v1/equipmentexercises:readpaginated

The global equipment catalog, alphabetical.

Parameters

NameInTypeDescription
pagequeryinteger1-based page number. Default: 1
pageSizequeryintegerItems per page, 1–100. Default: 30

Response — Equipment (as items[] in the pagination envelope)

FieldTypeDescription
idintegerStable catalog id.
namestring
subtitlestring | null
categorystringcamelCase enum.
GET/api/v1/equipment/{id}exercises:read

One equipment item by catalog id.

Parameters

NameInTypeDescription
idrequiredpathintegerCatalog equipment id.

Response — Equipment

FieldTypeDescription
…same as the list item

OAuth

Token endpoints are form-encoded (application/x-www-form-urlencoded) and follow RFC 6749/7009. The full flow is documented on the OAuth apps page.

GET/oauth/authorize

Hosted consent screen — starts the authorization-code flow. The user signs in and approves; their browser is redirected back with ?code=gac_…&state=….

Parameters

NameInTypeDescription
client_idrequiredquerystringYour app’s client id.
redirect_urirequiredquerystringMust exactly match a registered redirect URI.
scopequerystringSpace-separated scopes. Omit for all scopes your app is allowed.
statequerystringRandom value echoed back — verify it on return (CSRF protection).
code_challengequerystringPKCE S256 challenge. Required for public clients.
code_challenge_methodquerystringOnly "S256" is supported.

Response — Redirect

FieldTypeDescription
codestringSingle-use authorization code (gac_…), valid 10 minutes. Returned on the redirect, not as JSON.
statestringEchoed from the request.
POST/oauth/token

Exchange an authorization code, or rotate a refresh token, for an access/refresh token pair.

Parameters

NameInTypeDescription
grant_typerequiredformstring"authorization_code" or "refresh_token".
client_idrequiredformstring
client_secretformstringConfidential clients only; public clients omit it (PKCE instead).
codeformstringauthorization_code grant: the gac_… code.
redirect_uriformstringauthorization_code grant: must match the URI the code was issued for.
code_verifierformstringauthorization_code grant: the PKCE verifier for your challenge.
refresh_tokenformstringrefresh_token grant: the grt_… token (single use — rotated on success).

Response — Token response

FieldTypeDescription
access_tokenstringBearer token (gat_…), valid 6 hours.
token_type"Bearer"
expires_inintegerSeconds until the access token expires.
refresh_tokenstringNew rotating refresh token (grt_…), valid up to 180 days.
scopestringSpace-separated granted scopes.
Example response
{
  "access_token": "gat_…",
  "token_type": "Bearer",
  "expires_in": 21600,
  "refresh_token": "grt_…",
  "scope": "workouts:read stats:read"
}
POST/oauth/revoke

Revoke a token (RFC 7009). Revoking a refresh token revokes the whole grant. Returns 200 even for unknown tokens.

Parameters

NameInTypeDescription
tokenrequiredformstringThe access or refresh token to revoke.
client_idrequiredformstring
client_secretformstringConfidential clients only.

Response — Empty

FieldTypeDescription
(no body)200 on success; 401 invalid_client for bad credentials.