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
| Method | Path | 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 | — |
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
/api/v1/userprofile:readThe authorizing user’s profile: display fields, preferred units and training goal.
Response — User profile
| Field | Type | Description |
|---|---|---|
| id | string | Stable user identifier — key your storage on this. |
| displayName | string | null | |
| username | string | null | |
| weightUnit | "kilograms" | "pounds" | Preferred display unit — API weights are always pounds. |
| distanceUnit | "kilometers" | "miles" | |
| gender | "male" | "female" | "other" | null | |
| height | number | null | In the unit given by 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"
}Workouts
/api/v1/workoutsworkouts:readpaginatedThe user’s workout history, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
startDate | query | string (ISO 8601) | Only workouts starting on/after this instant (inclusive). |
endDate | query | string (ISO 8601) | Only workouts starting on/before this instant (inclusive). |
Response — Workout summary (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| id | string (uuid) | |
| name | string | |
| notes | string | null | |
| startDate | string (ISO 8601, UTC) | |
| endDate | string (ISO 8601, UTC) | |
| durationMinutes | integer | |
| type | string | Workout origin, e.g. "custom", "external". New values may appear. |
| volume | number | Total volume lifted, pounds. |
| calories | number | |
| personalRecordCount | integer | PRs set in this workout. |
| 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:readOne workout in full: every exercise with its logged sets, in performed order.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The workout id from the list endpoint. |
Response — Workout detail
| Field | Type | Description |
|---|---|---|
| …all workout summary fields | Same as the list item, minus exerciseCount. | |
| exercises[].exerciseId | integer | Catalog exercise id. |
| exercises[].exerciseName | string | |
| exercises[].supersetId | integer | null | Exercises sharing an id were performed as a superset. |
| exercises[].sets[].order | integer | 1-based position within the exercise. |
| exercises[].sets[].reps | integer | Repetitions performed. |
| exercises[].sets[].weight | number | Pounds. |
| exercises[].sets[].duration | integer | null | Seconds, for timed sets. |
| exercises[].sets[].distance | integer | null | Meters, for distance sets. |
| exercises[].sets[].rpe | integer | null | Rate of perceived exertion (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:readpaginatedThe user’s saved workout templates, newest first. Sets are planned values, not logs.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
Response — Template summary (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| id | string (uuid) | |
| name | string | |
| description | string | null | |
| exerciseCount | integer | |
| exercises | array | Always empty in list responses — use the detail endpoint. |
/api/v1/workout-templates/{id}workouts:readOne template with its planned exercises and sets.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The template id from the list endpoint. |
Response — Template detail
| Field | Type | Description |
|---|---|---|
| …all template summary fields | ||
| exercises[].exerciseId | integer | |
| exercises[].exerciseName | string | |
| exercises[].supersetId | integer | null | |
| exercises[].sets[].order | integer | 1-based position within the exercise. |
| exercises[].sets[].reps | integer | null | Planned repetitions. |
| exercises[].sets[].weight | number | null | Pounds. |
| exercises[].sets[].duration | integer | null | Seconds, for timed sets. |
| exercises[].sets[].distance | integer | null | Meters, for distance sets. |
| exercises[].sets[].rpe | integer | null | Rate of perceived exertion (1–10). |
| exercises[].sets[].setType | "normal" | "warmup" | "dropSet" | "failure" |
/api/v1/splitssplits:readpaginatedThe user’s custom training splits, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
Response — Split summary (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| id | string (uuid) | |
| name | string | null | |
| description | string | null | |
| sessionCount | integer | |
| sessions | array | Always empty in list responses — use the detail endpoint. |
/api/v1/splits/{id}splits:readOne split with its sessions in order.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | string (uuid) | The split id from the list endpoint. |
Response — Split detail
| Field | Type | Description |
|---|---|---|
| …all split summary fields | ||
| sessions[].order | integer | |
| sessions[].type | "muscles" | "workoutTemplate" | |
| sessions[].muscles | string[] | Target muscles (camelCase, e.g. "chest"); set when type is muscles. |
| sessions[].workoutTemplateId | string (uuid) | null | Linked template; set when type is workoutTemplate. |
Records & measurements
/api/v1/personal-recordsrecords:readpaginatedThe user’s personal records, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
exerciseId | query | integer | Filter to one catalog exercise. |
Response — Personal record (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| exerciseId | integer | |
| exerciseName | string | |
| type | "oneRepMax" | "weightMax" | "volumeMax" | "repetitionMax" | "durationMax" | "distanceMax" | |
| reps | integer | null | |
| weight | number | null | Pounds. |
| duration | integer | null | Seconds. |
| distance | integer | null | Meters. |
| workoutId | string (uuid) | null | Workout the record was set in, when known. |
| 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:readpaginatedBody weight and circumference entries, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
startDate | query | string (ISO 8601) | Only entries dated on/after this (inclusive). |
endDate | query | string (ISO 8601) | Only entries dated on/before this (inclusive). |
Response — Measurement (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| id | string (uuid) | |
| date | string (ISO 8601 date) | |
| weightUnit | "kilograms" | "pounds" | Unit system of this entry: circumferences are inches with pounds, centimeters with kilograms. |
| weight | number | null | |
| bodyFat | number | null | Percent. |
| neck / shoulder / chest / bicep / forearm / abdomen / waist / glutes / thigh / calf | number | null | Circumferences in the entry’s unit system. |
Stats & trophies
/api/v1/statsstats:readThe user’s headline training stats in one call.
Response — Stats
| Field | Type | Description |
|---|---|---|
| streak.current | integer | Current streak, weeks. |
| streak.longest | integer | Longest streak ever, weeks. |
| streak.workoutsThisWeek | integer | |
| totalXp | integer | |
| trophyCount | integer |
{
"streak": { "current": 6, "longest": 14, "workoutsThisWeek": 3 },
"totalXp": 12480,
"trophyCount": 22
}/api/v1/trophiesstats:readpaginatedEarned trophies, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
Response — Trophy (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| type | string | Trophy kind, camelCase — e.g. "workout100", "streak5", "calendar15". New values may appear. |
| exerciseId | integer | null | Set for exercise-specific trophies. |
| exerciseName | string | null | |
| achievedAt | string (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’.
/api/v1/exercisesexercises:readpaginatedThe exercise catalog plus the user’s custom exercises, alphabetical.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
search | query | string | Case-insensitive name match (contains). Max 200 chars. |
Response — Exercise (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| id | integer | Stable catalog id — referenced by workouts, records and templates. |
| name | string | |
| description | string | null | |
| category | string | camelCase enum, e.g. "barbell", "dumbbell". |
| type | string | camelCase enum, e.g. "weight", "duration". |
| muscle | string | Primary muscle, camelCase, e.g. "chest". |
| secondaryMuscles | string[] | |
| equipment | string[] | Equipment names usable for this exercise. |
/api/v1/exercises/{id}exercises:readOne exercise by catalog id.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | integer | Catalog exercise id. |
Response — Exercise
| Field | Type | Description |
|---|---|---|
| …same as the list item |
/api/v1/equipmentexercises:readpaginatedThe global equipment catalog, alphabetical.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page | query | integer | 1-based page number. Default: 1 |
pageSize | query | integer | Items per page, 1–100. Default: 30 |
Response — Equipment (as items[] in the pagination envelope)
| Field | Type | Description |
|---|---|---|
| id | integer | Stable catalog id. |
| name | string | |
| subtitle | string | null | |
| category | string | camelCase enum. |
/api/v1/equipment/{id}exercises:readOne equipment item by catalog id.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
idrequired | path | integer | Catalog equipment id. |
Response — Equipment
| Field | Type | Description |
|---|---|---|
| …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.
/oauth/tokenExchange an authorization code, or rotate a refresh token, for an access/refresh token pair.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
grant_typerequired | form | string | "authorization_code" or "refresh_token". |
client_idrequired | form | string | |
client_secret | form | string | Confidential clients only; public clients omit it (PKCE instead). |
code | form | string | authorization_code grant: the gac_… code. |
redirect_uri | form | string | authorization_code grant: must match the URI the code was issued for. |
code_verifier | form | string | authorization_code grant: the PKCE verifier for your challenge. |
refresh_token | form | string | refresh_token grant: the grt_… token (single use — rotated on success). |
Response — Token response
| Field | Type | Description |
|---|---|---|
| access_token | string | Bearer token (gat_…), valid 6 hours. |
| token_type | "Bearer" | |
| expires_in | integer | Seconds until the access token expires. |
| refresh_token | string | New rotating refresh token (grt_…), valid up to 180 days. |
| scope | string | Space-separated granted scopes. |
{
"access_token": "gat_…",
"token_type": "Bearer",
"expires_in": 21600,
"refresh_token": "grt_…",
"scope": "workouts:read stats:read"
}/oauth/revokeRevoke a token (RFC 7009). Revoking a refresh token revokes the whole grant. Returns 200 even for unknown tokens.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
tokenrequired | form | string | The access or refresh token to revoke. |
client_idrequired | form | string | |
client_secret | form | string | Confidential clients only. |
Response — Empty
| Field | Type | Description |
|---|---|---|
| (no body) | 200 on success; 401 invalid_client for bad credentials. |