Open Workout API core
The routes every Open Workout server serves, from core.yaml (OpenAPI 3.1, version v0). Check a server against it with ows conformance; the quickstarts show it in use.
The REST API any Open Workout server serves over a person's workout log. Workouts are Open Workout Schema v0 documents (https://openworkout.dev/schema/v0/workout.json) whose exercises are Open Exercise IDs (https://openexercise.org).
A server is found from a 401: its WWW-Authenticate header names the OAuth protected resource metadata (RFC 9728), which names the authorization server (RFC 8414). Clients use OAuth 2.1 with PKCE. Errors are RFC 9457 problem details whose code says what went wrong. Requests that create things take an Idempotency-Key header, and a retry with the same key returns the first result with Idempotent-Replayed: true. Webhook deliveries are signed in OWS-Signature.
A server may add routes, fields and limits of its own; clients ignore what they don't know. ows conformance checks a server against this document's behaviour. Lifterclub (https://api.lifterclub.com) is the reference implementation.
Workouts
GET /v1/export
Download the whole log scope: workouts:read
Every workout in the log, newest first, as a file. json holds Open Workout Schema workouts,
and a server may add more of the record beside them. csv has one row per set, and
needs workouts:read; free text starting with =, +, - or @ gets a leading ', so spreadsheets
don't run it. Deleted workouts aren't in workouts or the CSV. A server may limit how often it runs. If it fails part way the connection is cut, so an incomplete file never
looks finished.
| Parameter | In | Type | Meaning |
|---|---|---|---|
format | query | string |
| Status | Meaning | Body |
|---|---|---|
200 | The file, sent as an attachment. | text/csv: string |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
GET /v1/workouts
List workouts, newest first scope: workouts:read
Totals cover the workouts on this page.
| Parameter | In | Type | Meaning |
|---|---|---|---|
from | query | string (date) | First day, YYYY-MM-DD in the user's timezone. Default 30 days before to. |
to | query | string (date) | Last day, inclusive. Default today. |
exercise | query | string | Only workouts with this exercise (name or ID), showing just its entries. |
limit | query | integer | Workouts per page. |
cursor | query | string | next_cursor from the previous page. |
updated_since | query | string (date-time) | The change feed: every workout whose stored record changed at or after this time, oldest change first, ignoring from, to and exercise. For mirroring a log; see include_deleted. |
include_deleted | query | boolean | With updated_since, also list the workouts deleted since then, as tombstones in deleted. |
| Status | Meaning | Body |
|---|---|---|
200 | A page of workouts. | application/json: WorkoutList |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
422 | exercise_ambiguous (with candidates) or idempotency_conflict. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
POST /v1/workouts
Log a workout scope: workouts:write
| Parameter | In | Type | Meaning |
|---|---|---|---|
Idempotency-Key | header | string | Any unique string, up to 200 characters. Retrying with the same key never writes twice. |
Body: WorkoutInput
| Status | Meaning | Body |
|---|---|---|
200 | An earlier request with the same Idempotency-Key; nothing new was stored. | application/json: Logged |
201 | Stored. | application/json: Logged |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
422 | exercise_ambiguous (with candidates) or idempotency_conflict. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
GET /v1/workouts/{id}
Get a workout scope: workouts:read
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) |
| Status | Meaning | Body |
|---|---|---|
200 | The workout. | application/json: Workout |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/json: BearerError |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
PATCH /v1/workouts/{id}
Change a workout's title, date or notes, or finish it scope: workouts:write
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) |
Body: WorkoutPatch
| Status | Meaning | Body |
|---|---|---|
200 | The workout after the change. | application/json: Logged |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
DELETE /v1/workouts/{id}
Delete a workout scope: workouts:write
Returns an undo token that restores it for 10 minutes.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) |
| Status | Meaning | Body |
|---|---|---|
200 | Deleted, with an undo token. | application/json: Deleted |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/json: BearerError |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
Webhooks
GET /v1/webhooks
List this app's webhooks on the account scope: workouts:read
Without secrets. A webhook belongs to the grant the token came from, so each app sees only its own.
| Status | Meaning | Body |
|---|---|---|
200 | The app's subscriptions. | application/json: WebhookList |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
POST /v1/webhooks
Be told when the log changes scope: workouts:read
Subscribes the calling app to changes in this person's log. Every minute, each changed workout becomes one signed POST to url carrying the kind of change and the workout's ID, never its content; read the workout afterwards. The secret in the response is shown once. Only a connected app can subscribe: not a personal key, not the web app. At most 5 per app and account. Ten failed deliveries in a row pause the subscription; subscribe again to resume.
Body: WebhookInput
| Status | Meaning | Body |
|---|---|---|
201 | Subscribed. Location names the subscription. | application/json: Webhook |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/json: BearerError |
429 | rate_limited. | application/problem+json: Problem |
DELETE /v1/webhooks/{id}
End a webhook scope: workouts:read
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string |
| Status | Meaning | Body |
|---|---|---|
204 | Deleted. | |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
Sets and entries
PATCH /v1/entries/{id}
Confirm an entry's exercise, or change its notes scope: workouts:write
Setting exercise_id resolves needs_confirmation, and the server may remember what the user meant.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) |
Body: EntryPatch
| Status | Meaning | Body |
|---|---|---|
200 | The workout after the change. | application/json: Logged |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
DELETE /v1/entries/{id}
Delete an exercise entry with its sets scope: workouts:write
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) |
| Status | Meaning | Body |
|---|---|---|
200 | Deleted, with an undo token. | application/json: Deleted |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
POST /v1/entries/{id}/sets
Add sets to an exercise entry scope: workouts:write
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) | |
Idempotency-Key | header | string | Any unique string, up to 200 characters. Retrying with the same key never writes twice. |
Body: EntrySetsRequest
| Status | Meaning | Body |
|---|---|---|
200 | An earlier request with the same Idempotency-Key; nothing new was stored. | application/json: Logged |
201 | The workout after the change. | application/json: Logged |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
422 | exercise_ambiguous (with candidates) or idempotency_conflict. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
PATCH /v1/sets/{id}
Change a set scope: workouts:write
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) |
Body: SetPatch
| Status | Meaning | Body |
|---|---|---|
200 | The workout after the change. | application/json: Logged |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
DELETE /v1/sets/{id}
Delete a set scope: workouts:write
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string (uuid) |
| Status | Meaning | Body |
|---|---|---|
200 | Deleted, with an undo token. | application/json: Deleted |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
POST /v1/undo
Restore what a delete removed scope: workouts:write
Body: UndoRequest
| Status | Meaning | Body |
|---|---|---|
200 | The workout after the change. | application/json: Logged |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
410 | The undo token is unknown, already used or expired. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
POST /v1/workouts/{id}/sets
Add sets for an exercise scope: workouts:write
Sets for an exercise the workout already has join its entry.
| Parameter | In | Type | Meaning |
|---|---|---|---|
id required | path | string | A workout ID, or current for the session in progress (started if there is none in the last 4 hours). |
Idempotency-Key | header | string | Any unique string, up to 200 characters. Retrying with the same key never writes twice. |
Body: AddSetsRequest
| Status | Meaning | Body |
|---|---|---|
200 | An earlier request with the same Idempotency-Key; nothing new was stored. | application/json: Logged |
201 | The workout after the change. | application/json: Logged |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
404 | not_found: no such resource in this account. | application/problem+json: Problem |
422 | exercise_ambiguous (with candidates) or idempotency_conflict. | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
History
GET /v1/exercises
Search the catalogue and the user's custom exercises scope: workouts:read
| Parameter | In | Type | Meaning |
|---|---|---|---|
query required | query | string | Name or shorthand, e.g. "rdl". |
equipment | query | string | |
muscle | query | string | |
limit | query | integer |
| Status | Meaning | Body |
|---|---|---|
200 | Best matches first. | application/json: object |
400 | invalid_input: field names what to fix. | application/problem+json: Problem |
401 | Missing, invalid or expired token, or the account no longer exists. | application/problem+json: Problem |
403 | The token lacks the scope (a bearer error), or the user hasn't given health-data consent
(consent_required). | application/problem+json: Problem |
429 | rate_limited. | application/problem+json: Problem |
Schemas
AddSetsRequest
| Field | Type | Meaning |
|---|---|---|
sets required | array of SetInput | |
exercise | string | As the user named it. |
exercise_id | string |
BearerError
| Field | Type | Meaning |
|---|---|---|
error required | one of invalid_request, invalid_token, insufficient_scope, server_error | |
error_description | string |
Candidate
| Field | Type | Meaning |
|---|---|---|
id required | string | |
is_custom required | boolean | |
measurement_type required | MeasurementType | |
name required | string | |
score required | number |
Confirmation
An exercise to confirm with the user, then fix with PATCH /v1/entries/{entry_id}.
| Field | Type | Meaning |
|---|---|---|
candidates required | array of Candidate | |
entry_id required | string (uuid) | |
status required | MatchStatus | |
text required | string |
Date
string (date)
Deleted
| Field | Type | Meaning |
|---|---|---|
id required | string (uuid) | |
kind required | one of workout, exercise, set | |
undo_expires_at required | string (date-time) | |
undo_token required | string |
EntryPatch
| Field | Type | Meaning |
|---|---|---|
exercise_id | string | From needs_confirmation candidates or /v1/exercises. |
notes | string |
EntrySetsRequest
| Field | Type | Meaning |
|---|---|---|
sets required | array of SetInput |
ExerciseInput
One exercise and its sets: name and exercise_id, or an OWS entry's exercise and text.
| Field | Type | Meaning |
|---|---|---|
sets required | array of SetInput | |
exercise | object | An OWS entry's exercise. Its id is exercise_id; its name is used when neither name nor text is given; match is the server's and ignored. |
exercise_id | string | A catalogue or custom ID, when known; skips matching. |
id | string | A stored entry's ID, as an OWS document carries it. Ignored. |
name | string | The exercise as the user named it. |
notes | string | |
text | string | An OWS entry's text: what the person called the exercise. Used as name when name isn't given. |
ExerciseMatch
| Field | Type | Meaning |
|---|---|---|
equipment required | array of string | |
id required | string | |
is_custom required | boolean | |
measurement_type required | MeasurementType | |
name required | string | |
primary_muscles required | array of string | |
score required | number |
Load
load (../schema/v0/workout.json#/$defs/load)
Logged
The result of a write.
| Field | Type | Meaning |
|---|---|---|
needs_confirmation required | array of Confirmation | |
workout required | Workout | |
replayed | boolean |
MatchStatus
catalogue and custom are confirmed; close and unmatched need confirmation.
one of catalogue, close, custom, unmatched
MeasurementType
one of weight_reps, reps, duration, distance_duration, assisted_weight_reps
OWSExport
The log as the Open Workout core promises it, an OWS v0 log document (https://openworkout.dev/schema/v0/log.json). A server may add more of the person's record beside its keys.
log (../schema/v0/log.json)
Problem
| Field | Type | Meaning |
|---|---|---|
code required | one of invalid_input, consent_required, exercise_ambiguous, not_found, undo_unavailable, rate_limited, idempotency_conflict, unauthenticated, internal_error | |
status required | integer | |
title required | string | |
type required | string (uri) | |
candidates | array of Candidate | For exercise_ambiguous. |
detail | string | |
field | string | For invalid_input. |
ProgramRef
| Field | Type | Meaning |
|---|---|---|
id required | string | |
day | string | |
day_name | string | An OWS workout's program label. Ignored. |
listed | boolean | An OWS workout's program label. Ignored. |
name | string | An OWS workout's program label. Ignored. |
version | integer |
SetInput
A set as a client describes it, or as an OWS workout document has it.
| Field | Type | Meaning |
|---|---|---|
calories | integer | Kilocalories, only as a machine, watch or app reported them. |
distance_m | number | |
duration_s | integer | |
estimated | boolean | The values were inferred, not stated: read off a machine's screen, say. |
heart_rate_bpm | integer | Average heart rate over the set, only as a watch or app reported it. |
id | string | A stored set's ID, as an OWS document carries it. Ignored. |
load | number or Load | Leave out for bodyweight. A number with unit beside it, or an OWS load {value, unit}. |
reps | integer | |
rpe | number | |
type | one of working, warmup, backoff, amrap, failure, drop | The OWS set type; working when left out. Only warm-ups are left out of records. |
unit | Unit | Leave out for the user's unit. |
warmup | boolean | The same as type warmup. Warm-ups don't count toward records. |
SetPatch
| Field | Type | Meaning |
|---|---|---|
calories | integer | |
distance_m | number | |
duration_s | integer | |
heart_rate_bpm | integer | |
load | number | |
remove_load | boolean | Make it bodyweight. |
reps | integer | |
rpe | number | |
unit | Unit | |
warmup | boolean |
Tombstone
A deleted workout in the change feed, enough to remove a mirror's copy and nothing of what it held.
| Field | Type | Meaning |
|---|---|---|
deleted_at required | string (date-time) | |
id required | string | |
updated_at required | string (date-time) | |
merged_into | string | The workout it was merged into, when another app's record of the same session absorbed it. |
UndoRequest
| Field | Type | Meaning |
|---|---|---|
undo_token required | string |
Unit
one of kg, lb
Webhook
| Field | Type | Meaning |
|---|---|---|
consecutive_failures required | integer | |
created_at required | string (date-time) | |
events required | array of one of workout.created, workout.updated, workout.deleted | |
id required | string | |
url required | string | |
last_delivered_at | string (date-time) | |
last_status | integer | The endpoint's last HTTP status; 0 when it couldn't be reached. |
paused_at | string (date-time) | Set after ten failed deliveries in a row. Nothing is delivered until the app subscribes again. |
secret | string | Only in the creation response: the key the deliveries are signed with. OWS-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">. |
WebhookInput
| Field | Type | Meaning |
|---|---|---|
events required | array of one of workout.created, workout.updated, workout.deleted | |
url required | string | An https URL on a public host, with no credentials. Checked again on every delivery, on the address actually reached. |
WebhookList
| Field | Type | Meaning |
|---|---|---|
webhooks required | array of Webhook |
Workout
workout (../schema/v0/workout.json)
WorkoutInput
A workout to log. An OWS workout document can be posted as it is: the members the server assigns (id, updated_at, merged_sources, source's kind and client, entries' and sets' id, an exercise's match, a program label's name, day_name and listed) are ignored.
| Field | Type | Meaning |
|---|---|---|
exercises required | array of ExerciseInput | |
$schema | any | |
date | Date | Day in the workout's timezone, when started_at is not given. Default today. The start is then worked out and listed in estimates as /started_at. |
estimates | array of string | JSON pointers (RFC 6901) into this body to values you inferred rather than were told: a set, or a value in one (the set is marked), a set's /load/unit (the unit was assumed), or /started_at. |
external_id | string | Your app's own ID for this workout. A second POST with the same ID replays the first (200, Idempotent-Replayed: true) instead of logging twice; one the person has since deleted here answers 404 and stays deleted. Not for the web app. |
id | string | A stored workout's ID, as an OWS document carries it. Ignored. |
merged_sources | array of object | Ignored: merges are the server's. |
notes | string | |
program | ProgramRef | |
source | object | An OWS document's source. Its external_id is read as external_id; kind and client come from your credential. |
started_at | string (date-time) | |
timezone | string | The IANA time zone the workout happened in (America/Chicago), which decides its local day. Default the person's. |
title | string | |
updated_at | string (date-time) | Ignored: the server's clock sets it. |
WorkoutList
| Field | Type | Meaning |
|---|---|---|
truncated required | boolean | More workouts follow; pass next_cursor. |
workouts required | array of Workout | |
deleted | array of Tombstone | Workouts deleted since updated_since, when include_deleted was asked for. |
exercise | string | The exercise filter's name. |
from | Date | First day listed; absent in the change feed. |
next_cursor | string | |
to | Date | Last day listed; absent in the change feed. |
updated_since | string (date-time) | The change feed's start, when listing by updated_since. |
WorkoutPatch
| Field | Type | Meaning |
|---|---|---|
date | Date | Move the workout to this day. |
ended_at | string (date-time) | Finish the workout (a live session). Not before it started, nor in the future; POST /v1/workouts/current/sets then starts a new one. |
notes | string | |
title | string |