curl -X PATCH "https://api.fitsociety.io/public/v1/workout/performance/sessions/{sessionId}/complete" \
-H "Authorization: Bearer <access_token>" \
-H "Idempotency-Key: <stable_request_key>" \
-H "Content-Type: application/json" \
-d '{}'const options = {
method: 'PATCH',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({wodResult: {elapsedSeconds: 742, finishedBeforeCap: true, totalReps: 150}})
};
fetch('https://api.fitsociety.io/public/v1/workout/performance/sessions/{sessionId}/complete', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.fitsociety.io/public/v1/workout/performance/sessions/{sessionId}/complete"
payload = { "wodResult": {
"elapsedSeconds": 742,
"finishedBeforeCap": True,
"totalReps": 150
} }
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text){
"data": {
"sessionId": "67f1234567890abcdef1234",
"scoring": {
"scoringType": "forTime",
"descriptionOnly": true,
"acceptsManualResult": true,
"targetValue": null,
"timeDomain": {
"windowSeconds": null,
"timeCapSeconds": 900,
"rounds": 3,
"intervalSeconds": null,
"intervalCount": null,
"workSeconds": null,
"restSeconds": null
},
"scoreValidation": {
"timeCapSeconds": 900,
"maxReps": null,
"expectedIntervals": null
},
"result": {
"roundsCompleted": 5,
"repsPerRound": 30,
"extraReps": 12,
"totalReps": 162,
"intervalReps": [
12,
11,
10
],
"elapsedSeconds": 742,
"finishedBeforeCap": true,
"timeCapSeconds": 900,
"maxLoadKg": 120
}
},
"leaderboardEntryId": "67f1234567890abcdef1234"
},
"meta": {
"requestId": "4f849d7d-f4f1-45cc-b4b7-3984a3d17f83",
"rateLimit": {
"limit": 10,
"remaining": 9,
"resetSeconds": 1
},
"idempotency": {
"replayed": false
}
}
}Complete a workout v2 session
A client completing an exercise-based assigned or freestyle session must first save at least one completed set belonging to that session and client/company. Otherwise SESSION_COMPLETED_SET_REQUIRED (400) leaves the session open without completion side effects. Prescribed metrics and exercise-level isCompleted flags do not qualify. Explicit coach completion, score-based WODs, genuinely exercise-free plan days and retries of already-completed sessions keep their existing behavior.
Requires the workout_sessions:write scope. This operation maps to /app/v1/workout/performance/sessions/:sessionId/complete and retains its Workout V2 permission, feature-flag, and resource-scope checks.
The clientId path parameter is resolved inside the company bound to the Public API token when present.
curl -X PATCH "https://api.fitsociety.io/public/v1/workout/performance/sessions/{sessionId}/complete" \
-H "Authorization: Bearer <access_token>" \
-H "Idempotency-Key: <stable_request_key>" \
-H "Content-Type: application/json" \
-d '{}'const options = {
method: 'PATCH',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({wodResult: {elapsedSeconds: 742, finishedBeforeCap: true, totalReps: 150}})
};
fetch('https://api.fitsociety.io/public/v1/workout/performance/sessions/{sessionId}/complete', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.fitsociety.io/public/v1/workout/performance/sessions/{sessionId}/complete"
payload = { "wodResult": {
"elapsedSeconds": 742,
"finishedBeforeCap": True,
"totalReps": 150
} }
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.patch(url, json=payload, headers=headers)
print(response.text){
"data": {
"sessionId": "67f1234567890abcdef1234",
"scoring": {
"scoringType": "forTime",
"descriptionOnly": true,
"acceptsManualResult": true,
"targetValue": null,
"timeDomain": {
"windowSeconds": null,
"timeCapSeconds": 900,
"rounds": 3,
"intervalSeconds": null,
"intervalCount": null,
"workSeconds": null,
"restSeconds": null
},
"scoreValidation": {
"timeCapSeconds": 900,
"maxReps": null,
"expectedIntervals": null
},
"result": {
"roundsCompleted": 5,
"repsPerRound": 30,
"extraReps": 12,
"totalReps": 162,
"intervalReps": [
12,
11,
10
],
"elapsedSeconds": 742,
"finishedBeforeCap": true,
"timeCapSeconds": 900,
"maxLoadKg": 120
}
},
"leaderboardEntryId": "67f1234567890abcdef1234"
},
"meta": {
"requestId": "4f849d7d-f4f1-45cc-b4b7-3984a3d17f83",
"rateLimit": {
"limit": 10,
"remaining": 9,
"resetSeconds": 1
},
"idempotency": {
"replayed": false
}
}
}Authorizations
Public API access token issued by /public/v1/oauth/token. Example: Authorization: Bearer fspt_.... Each resource request rechecks the token company's current provider access. Disabling access blocks existing tokens with 403 auth.provider_unavailable.
Headers
Required for Public API write requests. Reusing the same key with the same method, path, and body replays the stored successful response; reusing it with a different request returns 409 idempotency.conflict.
1 - 200"booking-create-20260714-001"
Path Parameters
Workout session id.
"67f1234567890abcdef1234"
Body
"Session felt good overall."
Required when a coach completes on a client's behalf. A client token completes its own session and may omit it.
"67f1234567890abcdef1111"
The "Ask for feedback" toggle. When true AND the caller is a coach, the client is sent a push notification asking them to submit feedback, in their own language (en, nl, fr, de, es). Sent for plan days and freestyle sessions only: the title is the plan name for a plan-linked session, or "Freestyle Workout" for a genuinely ad-hoc one (no plan, WOD or group attachment — the same test the freestyle history feed uses). A WOD or group run sends nothing at all, since neither is described by this copy. Its data payload carries type: "workout_feedback_request", sessionId, clientId, companyId, historyType and dayTitle, so tapping it opens the workout-day history screen. historyType is "plan" or "freestyle" and is ALWAYS present — branch on it rather than inferring the kind from dayTitle. dayTitle is the plan day's name, or the freestyle history feed's own "Freestyle Workout N" label (the client's own session name when they set one); it is omitted when neither applies. Ignored when a client completes their own session, since the copy states the coach completed it. A push failure is logged and never fails the completion.
true
Manual score for an exercise-free scored plan day (scoring.acceptsManualResult on the day). Required to complete such a day, whoever completes it (client or coach); without it the call fails with WOD_RESULT_REQUIRED and the session stays open. Group-run and feedback auto-completion apply the same rule, so complete a scored day here first. Rejected with WOD_RESULT_NOT_ALLOWED on any other session (WOD sessions complete via PATCH /app/v1/wod/{id}/complete). The score and the completion are saved in one write. Once the session is completed its score is final: a retry returns the stored score and ignores any new one.
Show child attributes
Show child attributes