API and webhooks
Connect NetFit to your own tools. Every request acts as a signed-in user and sees only what that user sees in the app.
Authentication
Send the access token of a signed-in NetFit user as a bearer token in the Authorization header. The token expires: refresh it the way the app does.
Authorization: Bearer ACCESS_TOKEN
Endpoints
62 endpoints
Webhooks
GET
/api/webhook-endpointsList the coach's webhook endpoints (never the secret)
Response 200
WebhookEndpointList{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "active": { "type": "boolean" }, "consecutiveFailures": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, "description": "Final failed deliveries in a row since the last success" }, "disabledAt": { "description": "When NetFit switched the endpoint off after repeated failures", "type": [ "string", "null" ] }, "disabledReason": { "anyOf": [ { "type": "string", "enum": [ "consecutive_failures" ] }, { "type": "null" } ] }, "secretRotatedAt": { "type": [ "string", "null" ] }, "previousSecretExpiresAt": { "description": "Until when deliveries also carry a v1 signed with the previous secret", "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "url", "events", "active", "consecutiveFailures", "disabledAt", "disabledReason", "secretRotatedAt", "previousSecretExpiresAt", "createdAt", "updatedAt" ], "additionalProperties": false } }POST
/api/webhook-endpointsAdd a webhook endpoint; the signing secret is returned once
Request body
WebhookEndpointCreate{ "type": "object", "properties": { "url": { "type": "string", "minLength": 1, "maxLength": 2048 }, "events": { "minItems": 1, "maxItems": 5, "type": "array", "items": { "type": "string", "enum": [ "workout.completed", "checkin.submitted", "booking.created", "booking.cancelled", "client.joined" ] } } }, "required": [ "url", "events" ], "additionalProperties": false }Response 201
WebhookEndpointCreated{ "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "active": { "type": "boolean" }, "consecutiveFailures": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, "description": "Final failed deliveries in a row since the last success" }, "disabledAt": { "description": "When NetFit switched the endpoint off after repeated failures", "type": [ "string", "null" ] }, "disabledReason": { "anyOf": [ { "type": "string", "enum": [ "consecutive_failures" ] }, { "type": "null" } ] }, "secretRotatedAt": { "type": [ "string", "null" ] }, "previousSecretExpiresAt": { "description": "Until when deliveries also carry a v1 signed with the previous secret", "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" }, "secret": { "type": "string", "format": "starts_with", "pattern": "^whsec_.*" } }, "required": [ "id", "url", "events", "active", "consecutiveFailures", "disabledAt", "disabledReason", "secretRotatedAt", "previousSecretExpiresAt", "createdAt", "updatedAt", "secret" ], "additionalProperties": false }PATCH
/api/webhook-endpoints/{id}Change the URL or events of an endpoint, or switch it on or off (on also clears an automatic switch-off)
Request body
WebhookEndpointUpdate{ "type": "object", "properties": { "url": { "type": "string", "minLength": 1, "maxLength": 2048 }, "events": { "minItems": 1, "maxItems": 5, "type": "array", "items": { "type": "string", "enum": [ "workout.completed", "checkin.submitted", "booking.created", "booking.cancelled", "client.joined" ] } }, "active": { "type": "boolean" } }, "additionalProperties": false }Response 200
WebhookEndpoint{ "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "active": { "type": "boolean" }, "consecutiveFailures": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, "description": "Final failed deliveries in a row since the last success" }, "disabledAt": { "description": "When NetFit switched the endpoint off after repeated failures", "type": [ "string", "null" ] }, "disabledReason": { "anyOf": [ { "type": "string", "enum": [ "consecutive_failures" ] }, { "type": "null" } ] }, "secretRotatedAt": { "type": [ "string", "null" ] }, "previousSecretExpiresAt": { "description": "Until when deliveries also carry a v1 signed with the previous secret", "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" } }, "required": [ "id", "url", "events", "active", "consecutiveFailures", "disabledAt", "disabledReason", "secretRotatedAt", "previousSecretExpiresAt", "createdAt", "updatedAt" ], "additionalProperties": false }DELETE
/api/webhook-endpoints/{id}Remove an endpoint and its delivery log
Response 200
WebhookDeleteResponse{ "type": "object", "properties": { "deleted": { "type": "boolean", "const": true } }, "required": [ "deleted" ], "additionalProperties": false }GET
/api/webhook-endpoints/{id}/deliveriesThe latest 50 deliveries to an endpoint, newest first
Response 200
WebhookDeliveryList{ "maxItems": 50, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "event": { "type": "string" }, "status": { "type": "string", "enum": [ "pending", "delivered", "failed" ] }, "attempts": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "lastStatusCode": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "lastError": { "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "deliveredAt": { "type": [ "string", "null" ] }, "nextAttemptAt": { "type": "string" }, "replayOfId": { "description": "The delivery this one replays", "type": [ "string", "null" ] } }, "required": [ "id", "event", "status", "attempts", "lastStatusCode", "lastError", "createdAt", "deliveredAt", "nextAttemptAt", "replayOfId" ], "additionalProperties": false } }POST
/api/webhook-endpoints/{id}/deliveries/{deliveryId}/replaySend a finished (delivered or failed) delivery again, as a copy with a new delivery id; 409 while it is still pending or the endpoint is off
Response 202
WebhookReplayResponse{ "type": "object", "properties": { "id": { "type": "string" }, "event": { "type": "string" }, "status": { "type": "string", "const": "pending" }, "createdAt": { "type": "string" }, "replayOfId": { "type": "string" } }, "required": [ "id", "event", "status", "createdAt", "replayOfId" ], "additionalProperties": false }POST
/api/webhook-endpoints/{id}/rotate-secretReplace the signing secret; the old one keeps signing as a second v1 for 24 hours unless revokePrevious is true
Request body
WebhookSecretRotate{ "type": "object", "properties": { "revokePrevious": { "type": "boolean" } }, "additionalProperties": false }Response 200
WebhookSecretRotated{ "type": "object", "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "events": { "type": "array", "items": { "type": "string" } }, "active": { "type": "boolean" }, "consecutiveFailures": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, "description": "Final failed deliveries in a row since the last success" }, "disabledAt": { "description": "When NetFit switched the endpoint off after repeated failures", "type": [ "string", "null" ] }, "disabledReason": { "anyOf": [ { "type": "string", "enum": [ "consecutive_failures" ] }, { "type": "null" } ] }, "secretRotatedAt": { "type": [ "string", "null" ] }, "previousSecretExpiresAt": { "description": "Until when deliveries also carry a v1 signed with the previous secret", "type": [ "string", "null" ] }, "createdAt": { "type": "string" }, "updatedAt": { "type": "string" }, "secret": { "type": "string", "format": "starts_with", "pattern": "^whsec_.*" } }, "required": [ "id", "url", "events", "active", "consecutiveFailures", "disabledAt", "disabledReason", "secretRotatedAt", "previousSecretExpiresAt", "createdAt", "updatedAt", "secret" ], "additionalProperties": false }POST
/api/webhook-endpoints/{id}/testQueue a signed `ping` delivery to an active endpoint
Response 202
WebhookTestResponse{ "type": "object", "properties": { "id": { "type": "string" }, "event": { "type": "string", "const": "ping" }, "status": { "type": "string", "const": "pending" }, "createdAt": { "type": "string" } }, "required": [ "id", "event", "status", "createdAt" ], "additionalProperties": false }
Clients
GET
/api/clientsA coach's client roster, or a client's coaches
Response 200
ClientListResponse{ "anyOf": [ { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "trainerId": { "type": "string" }, "status": { "type": "string", "description": "Link status; `ended` links are never listed" }, "notes": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "client": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "email": { "type": "string" }, "avatar": { "type": [ "string", "null" ] }, "bio": { "type": [ "string", "null" ] }, "phone": { "type": [ "string", "null" ] }, "dateOfBirth": { "type": [ "string", "null" ] }, "height": { "type": [ "number", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" } }, "required": [ "id", "name", "email", "avatar", "bio", "phone", "dateOfBirth", "height", "createdAt" ], "additionalProperties": {} }, "risk": { "description": "W4: the at-risk score; null for a link that is not active", "anyOf": [ { "type": "object", "properties": { "score": { "type": "integer", "minimum": 0, "maximum": 100 }, "band": { "type": "string", "enum": [ "low", "medium", "high" ] }, "reasons": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string", "description": "Locale key, `insights.risk.reasons.<reason>`" }, "weight": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "detail": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": { "type": "number" }, "description": "ICU values for the locale key" } }, "required": [ "key", "weight", "detail" ], "additionalProperties": false } } }, "required": [ "score", "band", "reasons" ], "additionalProperties": false }, { "type": "null" } ] } }, "required": [ "id", "clientId", "trainerId", "status", "notes", "createdAt", "updatedAt", "client" ], "additionalProperties": {} }, "description": "`?trainerId=`: the coach roster" }, { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "trainerId": { "type": "string" }, "status": { "type": "string", "description": "Link status; `ended` links are never listed" }, "notes": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "trainer": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "email": { "type": "string" }, "avatar": { "type": [ "string", "null" ] }, "bio": { "type": [ "string", "null" ] }, "specializations": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "email", "avatar", "bio", "specializations" ], "additionalProperties": {} } }, "required": [ "id", "clientId", "trainerId", "status", "notes", "createdAt", "updatedAt", "trainer" ], "additionalProperties": {} }, "description": "`?clientId=`: the client's coaches" } ] }GET
/api/clients/{id}One client's profile with their latest metrics, nutrition, tasks and workouts
Response 200
ClientDetailResponse{ "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "email": { "type": "string" }, "avatar": { "type": [ "string", "null" ] }, "bio": { "type": [ "string", "null" ] }, "phone": { "type": [ "string", "null" ] }, "dateOfBirth": { "type": [ "string", "null" ] }, "height": { "type": [ "number", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "metricEntries": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" } }, "required": [ "id" ], "additionalProperties": {} }, "description": "Latest 10" }, "nutritionEntries": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" } }, "required": [ "id" ], "additionalProperties": {} }, "description": "Latest 10" }, "assignedTasks": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" } }, "required": [ "id" ], "additionalProperties": {} }, "description": "Latest 10" }, "assignedWorkoutsClient": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "workout": { "type": "object", "properties": { "id": { "type": "string" } }, "required": [ "id" ], "additionalProperties": {} } }, "required": [ "id", "workout" ], "additionalProperties": {} }, "description": "Latest 5, with the workout" } }, "required": [ "id", "name", "email", "avatar", "bio", "phone", "dateOfBirth", "height", "createdAt", "metricEntries", "nutritionEntries", "assignedTasks", "assignedWorkoutsClient" ], "additionalProperties": {} }GET
/api/clients/{id}/compliancePlanned sessions done over the last 7, 30 and 90 days, and whether it is dropping
Response 200
ComplianceResponse{ "type": "object", "properties": { "windows": { "type": "object", "properties": { "d7": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "parts": { "type": "object", "properties": { "training": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "nutrition": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "habits": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false } }, "required": [ "training" ], "additionalProperties": false } }, "required": [ "pct", "completed", "scheduled", "parts" ], "additionalProperties": false }, "d30": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "parts": { "type": "object", "properties": { "training": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "nutrition": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "habits": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false } }, "required": [ "training" ], "additionalProperties": false } }, "required": [ "pct", "completed", "scheduled", "parts" ], "additionalProperties": false }, "d90": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "parts": { "type": "object", "properties": { "training": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "nutrition": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "habits": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false } }, "required": [ "training" ], "additionalProperties": false } }, "required": [ "pct", "completed", "scheduled", "parts" ], "additionalProperties": false } }, "required": [ "d7", "d30", "d90" ], "additionalProperties": false, "description": "Each window ends yesterday" }, "previous": { "type": "object", "properties": { "d7": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "parts": { "type": "object", "properties": { "training": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "nutrition": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "habits": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false } }, "required": [ "training" ], "additionalProperties": false } }, "required": [ "pct", "completed", "scheduled", "parts" ], "additionalProperties": false }, "d30": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "parts": { "type": "object", "properties": { "training": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "nutrition": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "habits": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false } }, "required": [ "training" ], "additionalProperties": false } }, "required": [ "pct", "completed", "scheduled", "parts" ], "additionalProperties": false }, "d90": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "parts": { "type": "object", "properties": { "training": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "nutrition": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false }, "habits": { "type": "object", "properties": { "pct": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null when nothing was planned" }, "completed": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 }, "scheduled": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "required": [ "pct", "completed", "scheduled" ], "additionalProperties": false } }, "required": [ "training" ], "additionalProperties": false } }, "required": [ "pct", "completed", "scheduled", "parts" ], "additionalProperties": false } }, "required": [ "d7", "d30", "d90" ], "additionalProperties": false, "description": "The same windows, shifted back by their length" }, "dropFlag": { "type": "boolean", "description": "The 30-day figure fell 20 points or more" }, "dropPct": { "type": "integer", "minimum": 0, "maximum": 9007199254740991, "description": "Points the 30-day figure fell by; 0 when it held" } }, "required": [ "windows", "previous", "dropFlag", "dropPct" ], "additionalProperties": false }GET
/api/readinessHow ready to train a client is today, from sleep, mood and energy and training load
Response 200
ReadinessResponse{ "type": "object", "properties": { "date": { "type": "string" }, "score": { "anyOf": [ { "type": "integer", "minimum": 0, "maximum": 100 }, { "type": "null" } ], "description": "null without sleep or a check-in" }, "band": { "anyOf": [ { "type": "string", "enum": [ "low", "medium", "high" ] }, { "type": "null" } ] }, "inputs": { "type": "array", "items": { "type": "object", "properties": { "key": { "type": "string", "enum": [ "sleep", "wellbeing", "load", "hrv" ] }, "value": { "type": "number" }, "contribution": { "type": "number", "description": "Points of the score; the contributions add up to it" } }, "required": [ "key", "value", "contribution" ], "additionalProperties": false } }, "missing": { "type": "array", "items": { "type": "string", "enum": [ "sleep", "wellbeing", "load", "hrv" ] } } }, "required": [ "date", "score", "band", "inputs", "missing" ], "additionalProperties": false }
Workouts
GET
/api/assigned-workoutsWorkouts assigned to clients (the item count, not the items)
Response 200
AssignedWorkoutListResponse{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "workoutId": { "type": "string" }, "clientId": { "type": "string" }, "trainerId": { "type": "string" }, "scheduledDate": { "anyOf": [ { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, { "type": "null" } ] }, "status": { "type": "string" }, "clientNotes": { "type": [ "string", "null" ] }, "completedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] }, "programmeAssignmentId": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "client": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, "trainer": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, "workout": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "category": { "type": [ "string", "null" ] }, "difficulty": { "type": "string" }, "estimatedDuration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "_count": { "type": "object", "properties": { "items": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } }, "required": [ "items" ], "additionalProperties": false } }, "required": [ "id", "name", "description", "category", "difficulty", "estimatedDuration", "_count" ], "additionalProperties": {} } }, "required": [ "id", "workoutId", "clientId", "trainerId", "scheduledDate", "status", "clientNotes", "completedAt", "programmeAssignmentId", "createdAt", "updatedAt", "client", "trainer", "workout" ], "additionalProperties": {} } }GET
/api/assigned-workouts/{id}One assignment with the full prescription
Response 200
AssignedWorkoutDetailResponse{ "type": "object", "properties": { "id": { "type": "string" }, "workoutId": { "type": "string" }, "clientId": { "type": "string" }, "trainerId": { "type": "string" }, "scheduledDate": { "anyOf": [ { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, { "type": "null" } ] }, "status": { "type": "string" }, "clientNotes": { "type": [ "string", "null" ] }, "completedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] }, "programmeAssignmentId": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "client": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, "trainer": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, "workout": { "type": "object", "properties": { "id": { "type": "string" }, "trainerId": { "type": [ "string", "null" ] }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "category": { "type": [ "string", "null" ] }, "difficulty": { "type": "string" }, "estimatedDuration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "Minutes" }, "isTemplate": { "type": "boolean" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "workoutId": { "type": "string" }, "exerciseId": { "type": "string" }, "sets": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "reps": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "weight": { "type": [ "number", "null" ] }, "restTime": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "duration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "order": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "notes": { "type": [ "string", "null" ] }, "exercise": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "category": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "category" ], "additionalProperties": {} } }, "required": [ "id", "workoutId", "exerciseId", "sets", "reps", "weight", "restTime", "duration", "order", "notes", "exercise" ], "additionalProperties": {} } } }, "required": [ "id", "trainerId", "name", "description", "category", "difficulty", "estimatedDuration", "isTemplate", "createdAt", "updatedAt", "items" ], "additionalProperties": {} } }, "required": [ "id", "workoutId", "clientId", "trainerId", "scheduledDate", "status", "clientNotes", "completedAt", "programmeAssignmentId", "createdAt", "updatedAt", "client", "trainer", "workout" ], "additionalProperties": {} }GET
/api/workout-logsCompleted workout logs, a page at a time
Response 200
WorkoutLogPage{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "workoutId": { "type": [ "string", "null" ] }, "name": { "type": "string" }, "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "duration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "notes": { "type": [ "string", "null" ] }, "calories": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "exercises": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "exerciseId": { "type": [ "string", "null" ] }, "exerciseName": { "type": "string" }, "sets": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "reps": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "weight": { "type": [ "number", "null" ] }, "duration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "order": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "setLog": { "description": "With `?include=setLog`", "type": [ "string", "null" ] } }, "required": [ "id", "exerciseId", "exerciseName", "sets", "reps", "weight", "duration", "order" ], "additionalProperties": {} } } }, "required": [ "id", "clientId", "workoutId", "name", "date", "duration", "notes", "calories", "createdAt", "updatedAt", "exercises" ], "additionalProperties": {} } }, "nextCursor": { "description": "Pass as `?cursor=` for the next page; null on the last", "type": [ "string", "null" ] } }, "required": [ "items", "nextCursor" ], "additionalProperties": false }GET
/api/workout-logs/{id}One workout log with its exercises
Response 200
WorkoutLog{ "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "workoutId": { "type": [ "string", "null" ] }, "name": { "type": "string" }, "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "duration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "notes": { "type": [ "string", "null" ] }, "calories": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "exercises": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "exerciseId": { "type": [ "string", "null" ] }, "exerciseName": { "type": "string" }, "sets": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "reps": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "weight": { "type": [ "number", "null" ] }, "duration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "order": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "setLog": { "description": "With `?include=setLog`", "type": [ "string", "null" ] } }, "required": [ "id", "exerciseId", "exerciseName", "sets", "reps", "weight", "duration", "order" ], "additionalProperties": {} } } }, "required": [ "id", "clientId", "workoutId", "name", "date", "duration", "notes", "calories", "createdAt", "updatedAt", "exercises" ], "additionalProperties": {} }GET
/api/workoutsThe coach's workout templates with their exercises
Response 200
WorkoutListResponse{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "trainerId": { "type": [ "string", "null" ] }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "category": { "type": [ "string", "null" ] }, "difficulty": { "type": "string" }, "estimatedDuration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "Minutes" }, "isTemplate": { "type": "boolean" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "workoutId": { "type": "string" }, "exerciseId": { "type": "string" }, "sets": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "reps": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "weight": { "type": [ "number", "null" ] }, "restTime": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "duration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "order": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "notes": { "type": [ "string", "null" ] }, "exercise": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "category": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "category" ], "additionalProperties": {} } }, "required": [ "id", "workoutId", "exerciseId", "sets", "reps", "weight", "restTime", "duration", "order", "notes", "exercise" ], "additionalProperties": {} } } }, "required": [ "id", "trainerId", "name", "description", "category", "difficulty", "estimatedDuration", "isTemplate", "createdAt", "updatedAt", "items" ], "additionalProperties": {} } }GET
/api/workouts/{id}One workout with its items in order
Response 200
Workout{ "type": "object", "properties": { "id": { "type": "string" }, "trainerId": { "type": [ "string", "null" ] }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "category": { "type": [ "string", "null" ] }, "difficulty": { "type": "string" }, "estimatedDuration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "Minutes" }, "isTemplate": { "type": "boolean" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "workoutId": { "type": "string" }, "exerciseId": { "type": "string" }, "sets": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "reps": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "weight": { "type": [ "number", "null" ] }, "restTime": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "duration": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ] }, "order": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "notes": { "type": [ "string", "null" ] }, "exercise": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "category": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "category" ], "additionalProperties": {} } }, "required": [ "id", "workoutId", "exerciseId", "sets", "reps", "weight", "restTime", "duration", "order", "notes", "exercise" ], "additionalProperties": {} } } }, "required": [ "id", "trainerId", "name", "description", "category", "difficulty", "estimatedDuration", "isTemplate", "createdAt", "updatedAt", "items" ], "additionalProperties": {} }
Bookings
GET
/api/booking/public-pageThe coach's public booking page settings (coach only); the page is /book/<slug>
Response 200
PublicBookingPageSettings{ "type": "object", "properties": { "enabled": { "type": "boolean" }, "slug": { "type": [ "string", "null" ] } }, "required": [ "enabled", "slug" ], "additionalProperties": false }PUT
/api/booking/public-pageTurn the public booking page on or off, or change its slug (coach only)
Request body
PublicBookingPageUpdate{ "type": "object", "properties": { "enabled": { "type": "boolean" }, "slug": { "type": "string", "minLength": 3, "maxLength": 40, "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" } }, "additionalProperties": false }Response 200
PublicBookingPageSettings{ "type": "object", "properties": { "enabled": { "type": "boolean" }, "slug": { "type": [ "string", "null" ] } }, "required": [ "enabled", "slug" ], "additionalProperties": false }GET
/api/booking/waitlistLive 1:1 waitlist places: a client's own, or every place in a coach's diary
Response 200
BookingWaitlistListResponse{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "trainerId": { "type": "string" }, "clientId": { "type": "string" }, "sessionTypeId": { "type": "string" }, "startsAt": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, "status": { "type": "string", "enum": [ "waiting", "offered", "booked", "expired", "left" ] }, "offeredAt": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, { "type": "null" } ] }, "holdExpiresAt": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, "position": { "anyOf": [ { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, { "type": "null" } ] } }, "required": [ "id", "trainerId", "clientId", "sessionTypeId", "startsAt", "status", "offeredAt", "holdExpiresAt", "createdAt", "position" ], "additionalProperties": false } } }, "required": [ "items" ], "additionalProperties": false }POST
/api/booking/waitlistA client queues for a taken start of one of their coach's session types
Request body
BookingWaitlistJoin{ "type": "object", "properties": { "sessionTypeId": { "type": "string", "pattern": "^[cC][^\\s-]{8,}$" }, "startsAt": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" } }, "required": [ "sessionTypeId", "startsAt" ], "additionalProperties": false }Response 201
BookingWaitlistPlace{ "type": "object", "properties": { "id": { "type": "string" }, "trainerId": { "type": "string" }, "clientId": { "type": "string" }, "sessionTypeId": { "type": "string" }, "startsAt": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, "status": { "type": "string", "enum": [ "waiting", "offered", "booked", "expired", "left" ] }, "offeredAt": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, { "type": "null" } ] }, "holdExpiresAt": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, { "type": "null" } ] }, "createdAt": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, "position": { "anyOf": [ { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 }, { "type": "null" } ] } }, "required": [ "id", "trainerId", "clientId", "sessionTypeId", "startsAt", "status", "offeredAt", "holdExpiresAt", "createdAt", "position" ], "additionalProperties": false }DELETE
/api/booking/waitlist/{id}Leave a waitlist; an offer held is passed to the next in line
GET
/api/bookingsSessions overlapping a window: a coach's diary or a client's own
Response 200
BookingListResponse{ "maxItems": 1000, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "startsAt": { "type": "string", "description": "ISO-8601 timestamp" }, "endsAt": { "type": "string", "description": "ISO-8601 timestamp" }, "status": { "type": "string", "enum": [ "pending", "confirmed", "cancelled", "no_show", "attended", "late_cancel" ] }, "clientNote": { "type": [ "string", "null" ] }, "coachNote": { "description": "Coach only; absent for a client", "type": [ "string", "null" ] }, "notes": { "description": "Deprecated: read clientNote and coachNote", "type": [ "string", "null" ] }, "meetingUrl": { "type": [ "string", "null" ] }, "seriesId": { "type": [ "string", "null" ] }, "cancelReason": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "sessionType": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "color": { "type": [ "string", "null" ] }, "durationMin": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "mode": { "type": "string" } }, "required": [ "id", "name", "color", "durationMin", "mode" ], "additionalProperties": {} }, "trainer": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, "client": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, { "type": "null" } ] }, "location": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "address": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "address" ], "additionalProperties": {} }, { "type": "null" } ] } }, "required": [ "id", "startsAt", "endsAt", "status", "clientNote", "notes", "meetingUrl", "seriesId", "cancelReason", "createdAt", "sessionType", "trainer", "client", "location" ], "additionalProperties": {} } }GET
/api/bookings/{id}One booking
Response 200
BookingView{ "type": "object", "properties": { "id": { "type": "string" }, "startsAt": { "type": "string", "description": "ISO-8601 timestamp" }, "endsAt": { "type": "string", "description": "ISO-8601 timestamp" }, "status": { "type": "string", "enum": [ "pending", "confirmed", "cancelled", "no_show", "attended", "late_cancel" ] }, "clientNote": { "type": [ "string", "null" ] }, "coachNote": { "description": "Coach only; absent for a client", "type": [ "string", "null" ] }, "notes": { "description": "Deprecated: read clientNote and coachNote", "type": [ "string", "null" ] }, "meetingUrl": { "type": [ "string", "null" ] }, "seriesId": { "type": [ "string", "null" ] }, "cancelReason": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "sessionType": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "color": { "type": [ "string", "null" ] }, "durationMin": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "mode": { "type": "string" } }, "required": [ "id", "name", "color", "durationMin", "mode" ], "additionalProperties": {} }, "trainer": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, "client": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} }, { "type": "null" } ] }, "location": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "address": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "address" ], "additionalProperties": {} }, { "type": "null" } ] } }, "required": [ "id", "startsAt", "endsAt", "status", "clientNote", "notes", "meetingUrl", "seriesId", "cancelReason", "createdAt", "sessionType", "trainer", "client", "location" ], "additionalProperties": {} }
Check-ins
GET
/api/checkins/dueScheduled check-in occurrences and their status
Response 200
CheckinDueListResponse{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "scheduleId": { "type": "string" }, "clientId": { "type": "string" }, "dueDate": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "status": { "type": "string", "enum": [ "pending", "done", "missed", "skipped" ] }, "formResponseId": { "type": [ "string", "null" ] }, "remindedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] }, "completedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] }, "schedule": { "type": "object", "properties": { "id": { "type": "string" }, "cadence": { "type": "string", "enum": [ "once", "weekly", "biweekly", "monthly" ] }, "trainerId": { "type": "string" }, "form": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "description": { "type": [ "string", "null" ] } }, "required": [ "id", "title", "description" ], "additionalProperties": false } }, "required": [ "id", "cadence", "trainerId", "form" ], "additionalProperties": false } }, "required": [ "id", "scheduleId", "clientId", "dueDate", "status", "formResponseId", "remindedAt", "completedAt", "schedule" ], "additionalProperties": false } }GET
/api/form-responsesSubmitted check-in and form responses
Response 200
FormResponseListResponse{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "formId": { "type": "string" }, "clientId": { "type": "string" }, "answers": { "type": "string", "description": "JSON-encoded answers, keyed by question" }, "submittedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "form": { "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" } }, "required": [ "id", "title" ], "additionalProperties": {} }, "client": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} } }, "required": [ "id", "formId", "clientId", "answers", "submittedAt", "form", "client" ], "additionalProperties": {} } }POST
/api/forms/{id}/signE-sign your own submitted response to this form. Behind ESIGN_ENABLED: 404 while it is off
Request body
FormSign{ "type": "object", "properties": { "responseId": { "type": "string", "pattern": "^[cC][^\\s-]{8,}$" }, "signerName": { "type": "string", "minLength": 1, "maxLength": 200 }, "waiverAccepted": { "type": "boolean", "const": true }, "signatureImage": { "type": "string", "maxLength": 400000, "pattern": "^data:image\\/png;base64,[A-Za-z0-9+/=]+$" } }, "required": [ "responseId", "signerName", "waiverAccepted" ], "additionalProperties": false }Response 201
SignedDocumentCreated{ "type": "object", "properties": { "id": { "type": "string" }, "formId": { "type": [ "string", "null" ] }, "responseId": { "type": [ "string", "null" ] }, "userId": { "type": "string" }, "signerName": { "type": "string" }, "signedAt": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "pdfUrl": { "type": "string" }, "textUrl": { "type": "string" } }, "required": [ "id", "formId", "responseId", "userId", "signerName", "signedAt", "sha256", "pdfUrl", "textUrl" ], "additionalProperties": false }POST
/api/forms/parqAdd the PAR-Q health screen to the coach's forms (one copy per coach)
Request body
ParqFormCreate{ "type": "object", "properties": { "assignOnJoin": { "type": "boolean" } }, "additionalProperties": false }Response 201
ParqFormSummary{ "type": "object", "properties": { "id": { "type": "string" }, "title": { "type": "string" }, "assignOnJoin": { "type": "boolean" } }, "required": [ "id", "title", "assignOnJoin" ], "additionalProperties": false }GET
/api/signed-documents/{id}A signed form response (its signer, or the coach it was signed for)
Response 200
SignedDocumentView{ "type": "object", "properties": { "id": { "type": "string" }, "formId": { "type": [ "string", "null" ] }, "responseId": { "type": [ "string", "null" ] }, "userId": { "type": "string" }, "signerName": { "type": "string" }, "signedAt": { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" }, "intact": { "type": "boolean" }, "hasDrawnSignature": { "type": "boolean" }, "pdfUrl": { "type": "string" }, "textUrl": { "type": "string" } }, "required": [ "id", "formId", "responseId", "userId", "signerName", "signedAt", "sha256", "intact", "hasDrawnSignature", "pdfUrl", "textUrl" ], "additionalProperties": false }
Habits
GET
/api/habit-entriesA client's habit check-offs
Response 200
HabitEntryListResponse{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "habitId": { "type": "string" }, "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "completed": { "type": "boolean" }, "notes": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "habit": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "category": { "type": "string" }, "frequency": { "type": "string" }, "assigned": { "type": "boolean", "description": "False when the coach switched it off for this client" } }, "required": [ "id", "name", "category", "frequency", "assigned" ], "additionalProperties": false } }, "required": [ "id", "clientId", "habitId", "date", "completed", "notes", "createdAt", "habit" ], "additionalProperties": {} } }GET
/api/habitsA coach's habits with recent entries
Response 200
HabitListResponse{ "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "trainerId": { "type": "string" }, "name": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "frequency": { "type": "string" }, "category": { "type": "string" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "entries": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "habitId": { "type": "string" }, "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "completed": { "type": "boolean" }, "notes": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" } }, "required": [ "id", "clientId", "habitId", "date", "completed", "notes", "createdAt" ], "additionalProperties": {} }, "description": "Within `?days=` (default window)" }, "unassignments": { "description": "Coach only: clients this habit is switched off for", "type": "array", "items": { "type": "object", "properties": { "clientId": { "type": "string" } }, "required": [ "clientId" ], "additionalProperties": false } } }, "required": [ "id", "trainerId", "name", "description", "frequency", "category", "createdAt", "updatedAt", "entries" ], "additionalProperties": {} } }POST
/api/habits/packs/{slug}/applyAdd a habit pack (hydration, sleep, steps, protein, mobility, stress) to clients
Request body
HabitPackApply{ "type": "object", "properties": { "clientIds": { "minItems": 1, "maxItems": 200, "type": "array", "items": { "type": "string" } } }, "required": [ "clientIds" ], "additionalProperties": false }Response 200
HabitPackApplyResponse{ "type": "object", "properties": { "packSlug": { "type": "string" }, "clientIds": { "type": "array", "items": { "type": "string" } }, "habits": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "packItem": { "type": "string" }, "created": { "type": "boolean" } }, "required": [ "id", "name", "packItem", "created" ], "additionalProperties": false } } }, "required": [ "packSlug", "clientIds", "habits" ], "additionalProperties": false }
Metrics
GET
/api/metric-definitionsThe coach's custom metrics (a client sees their coaches')
Response 200
MetricDefinitionPage{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "trainerId": { "type": "string" }, "name": { "type": "string" }, "unit": { "type": "string" }, "archivedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" } }, "required": [ "id", "trainerId", "name", "unit", "archivedAt", "createdAt", "updatedAt" ], "additionalProperties": false } }, "nextCursor": { "description": "Pass as `?cursor=` for the next page; null on the last", "type": [ "string", "null" ] } }, "required": [ "items", "nextCursor" ], "additionalProperties": false }GET
/api/metricsBody metric entries, a page at a time
Response 200
MetricEntryPage{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "metricType": { "type": "string" }, "value": { "type": "number" }, "unit": { "type": "string" }, "notes": { "type": [ "string", "null" ] }, "definitionId": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "definition": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "unit": { "type": "string" }, "archivedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] } }, "required": [ "id", "name", "unit", "archivedAt" ], "additionalProperties": false }, { "type": "null" } ] } }, "required": [ "id", "clientId", "date", "metricType", "value", "unit", "notes", "definitionId", "createdAt", "updatedAt", "definition" ], "additionalProperties": {} } }, "nextCursor": { "description": "Pass as `?cursor=` for the next page; null on the last", "type": [ "string", "null" ] } }, "required": [ "items", "nextCursor" ], "additionalProperties": false }GET
/api/metrics/{id}One metric entry
Response 200
MetricEntryDetail{ "type": "object", "properties": { "id": { "type": "string" }, "clientId": { "type": "string" }, "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "metricType": { "type": "string" }, "value": { "type": "number" }, "unit": { "type": "string" }, "notes": { "type": [ "string", "null" ] }, "definitionId": { "type": [ "string", "null" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "updatedAt": { "type": "string", "description": "ISO-8601 timestamp" }, "client": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "avatar": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "avatar" ], "additionalProperties": {} } }, "required": [ "id", "clientId", "date", "metricType", "value", "unit", "notes", "definitionId", "createdAt", "updatedAt", "client" ], "additionalProperties": {} }
Messages
GET
/api/messagesOne thread between the caller and another person, a page at a time
Response 200
MessagePage{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "senderId": { "type": "string" }, "receiverId": { "type": "string" }, "content": { "type": "string" }, "messageType": { "type": "string", "enum": [ "text", "image", "video", "file", "encouragement", "audio" ] }, "isRead": { "type": "boolean" }, "readAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ], "description": "When the receiver read it (WP-05-03); null while unread, or read before receipts existed" }, "badgeCleared": { "type": "boolean", "description": "The receiver cleared the unread badge without opening it (mark all read, dismissing the summary, blocking): isRead is true but it was not read. Show a read receipt only for readAt, or isRead without badgeCleared" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "conversationId": { "type": [ "string", "null" ] }, "clientMessageId": { "type": [ "string", "null" ] }, "durationMs": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "Voice note (`audio`) only: its length in milliseconds, at most 420000" }, "waveform": { "type": "array", "items": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "description": "Voice note only: loudness peaks 0-100, at most 64; empty otherwise" } }, "required": [ "id", "senderId", "receiverId", "content", "messageType", "isRead", "readAt", "badgeCleared", "createdAt", "conversationId", "clientMessageId", "durationMs", "waveform" ], "additionalProperties": {} } }, "nextCursor": { "description": "Pass as `?cursor=` for the next page; null on the last", "type": [ "string", "null" ] } }, "required": [ "items", "nextCursor" ], "additionalProperties": false }PATCH
/api/messages/{id}Mark a message addressed to the caller read; stamps `readAt` once
Response 200
Message{ "type": "object", "properties": { "id": { "type": "string" }, "senderId": { "type": "string" }, "receiverId": { "type": "string" }, "content": { "type": "string" }, "messageType": { "type": "string", "enum": [ "text", "image", "video", "file", "encouragement", "audio" ] }, "isRead": { "type": "boolean" }, "readAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ], "description": "When the receiver read it (WP-05-03); null while unread, or read before receipts existed" }, "badgeCleared": { "type": "boolean", "description": "The receiver cleared the unread badge without opening it (mark all read, dismissing the summary, blocking): isRead is true but it was not read. Show a read receipt only for readAt, or isRead without badgeCleared" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "conversationId": { "type": [ "string", "null" ] }, "clientMessageId": { "type": [ "string", "null" ] }, "durationMs": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "Voice note (`audio`) only: its length in milliseconds, at most 420000" }, "waveform": { "type": "array", "items": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "description": "Voice note only: loudness peaks 0-100, at most 64; empty otherwise" } }, "required": [ "id", "senderId", "receiverId", "content", "messageType", "isRead", "readAt", "badgeCleared", "createdAt", "conversationId", "clientMessageId", "durationMs", "waveform" ], "additionalProperties": {} }GET
/api/messages/voice-notesWhether voice notes are on, and the limits a recorder must keep to
Response 200
VoiceNotesConfig{ "type": "object", "properties": { "enabled": { "type": "boolean", "description": "`VOICE_NOTES_ENABLED` on this deployment" }, "maxDurationMs": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "maxBytes": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "mimeTypes": { "type": "array", "items": { "type": "string" } }, "waveformPoints": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } }, "required": [ "enabled", "maxDurationMs", "maxBytes", "mimeTypes", "waveformPoints" ], "additionalProperties": false }
Nutrition
GET
/api/foods/favouritesThe caller's starred foods, newest first, each with the amount last logged
POST
/api/foods/favouritesStar a food: an Open Food Facts product or one of the caller's own foods
Request body
FoodFavouriteCreate{ "type": "object", "properties": { "offProductId": { "anyOf": [ { "type": "string", "pattern": "^[cC][^\\s-]{8,}$" }, { "type": "null" } ] }, "customFoodId": { "anyOf": [ { "type": "string", "pattern": "^[cC][^\\s-]{8,}$" }, { "type": "null" } ] } }, "additionalProperties": false }DELETE
/api/foods/favourites/{foodRef}Un-star a food; `foodRef` is `off:<id>` or `custom:<id>`
POST
/api/foods/recogniseSuggest what is in a meal photo, as editable estimates. Needs `ai_processing` consent (403 CONSENT_REQUIRED) and a configured provider (501 otherwise)
Request body
FoodRecognise{ "type": "object", "properties": { "photoUrl": { "type": "string", "minLength": 1, "maxLength": 2048 }, "lang": { "type": "string", "pattern": "^[a-z]{2}$" } }, "required": [ "photoUrl" ], "additionalProperties": false }GET
/api/nutrition/complianceHow closely a client ate to their targets over 7, 14 or 30 days
Response 200
NutritionComplianceResponse{ "type": "object", "properties": { "clientId": { "type": "string" }, "endDate": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "days": { "type": "array", "items": { "type": "object", "properties": { "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "status": { "type": "string", "enum": [ "on_track", "over", "under", "not_logged" ] }, "calories": { "type": "number" }, "targetCalories": { "type": "number" }, "loggedMeals": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } }, "required": [ "date", "status", "calories", "targetCalories", "loggedMeals" ], "additionalProperties": false } }, "compliancePct": { "type": "integer", "minimum": 0, "maximum": 100 }, "thresholdPct": { "type": "number" }, "daysLogged": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "avgCalories": { "type": [ "number", "null" ] }, "avgProtein": { "type": [ "number", "null" ] }, "avgCarbs": { "type": [ "number", "null" ] }, "avgFat": { "type": [ "number", "null" ] } }, "required": [ "clientId", "endDate", "days", "compliancePct", "thresholdPct", "daysLogged", "avgCalories", "avgProtein", "avgCarbs", "avgFat" ], "additionalProperties": false }POST
/api/nutrition/copyCopy a day or one meal of the diary to another date or meal (photos are not copied; notes, food links and micros are)
Request body
NutritionCopy{ "type": "object", "properties": { "clientId": { "type": "string" }, "from": { "type": "object", "properties": { "date": { "type": "string" }, "mealType": { "type": "string", "enum": [ "breakfast", "lunch", "dinner", "snack" ] } }, "required": [ "date" ], "additionalProperties": false }, "to": { "type": "object", "properties": { "date": { "type": "string" }, "mealType": { "type": "string", "enum": [ "breakfast", "lunch", "dinner", "snack" ] } }, "required": [ "date" ], "additionalProperties": false }, "mode": { "type": "string", "enum": [ "append", "replace" ] } }, "required": [ "from", "to" ], "additionalProperties": false }POST
/api/nutrition/from-planLog a meal of a meal plan the client follows on that date, with its linked food's extra nutrients and micros
Request body
NutritionFromPlan{ "type": "object", "properties": { "clientId": { "type": "string" }, "planId": { "type": "string", "pattern": "^[cC][^\\s-]{8,}$" }, "mealPlanMealId": { "type": "string", "pattern": "^[cC][^\\s-]{8,}$" }, "date": { "type": "string" }, "servings": { "type": "number", "minimum": 0.25, "maximum": 50 }, "loggedAt": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, { "type": "null" } ] }, "force": { "type": "boolean" } }, "required": [ "planId", "mealPlanMealId", "date" ], "additionalProperties": false }GET
/api/nutrition/reportA client's weekly nutrition report
Response 200
NutritionWeeklyReportResponse{ "type": "object", "properties": { "clientId": { "type": "string" }, "weekStart": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "weekEnd": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "daysLogged": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "compliancePct": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "averages": { "type": "object", "properties": { "calories": { "type": [ "number", "null" ] }, "protein": { "type": [ "number", "null" ] }, "carbs": { "type": [ "number", "null" ] }, "fat": { "type": [ "number", "null" ] }, "fiber": { "type": [ "number", "null" ] }, "sugar": { "type": [ "number", "null" ] }, "saturatedFat": { "type": [ "number", "null" ] }, "sodiumMg": { "type": [ "number", "null" ] }, "waterMl": { "type": [ "number", "null" ] } }, "required": [ "calories", "protein", "carbs", "fat", "fiber", "sugar", "saturatedFat", "sodiumMg", "waterMl" ], "additionalProperties": false }, "targets": { "type": "object", "properties": { "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" } }, "required": [ "calories", "protein", "carbs", "fat" ], "additionalProperties": false }, "bestDay": { "anyOf": [ { "type": "object", "properties": { "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "calories": { "type": "number" } }, "required": [ "date", "calories" ], "additionalProperties": false }, { "type": "null" } ] }, "worstDay": { "anyOf": [ { "type": "object", "properties": { "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "calories": { "type": "number" } }, "required": [ "date", "calories" ], "additionalProperties": false }, { "type": "null" } ] }, "topFoods": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "count": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "calories": { "type": "number" }, "source": { "type": "string" } }, "required": [ "name", "count", "calories", "source" ], "additionalProperties": false } }, "photosLogged": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "comments": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "body": { "type": "string" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" } }, "required": [ "id", "body", "createdAt" ], "additionalProperties": {} } }, "partialNutrients": { "type": "array", "items": { "type": "string" } }, "attribution": { "anyOf": [ { "type": "object", "properties": { "text": { "type": "string" }, "url": { "type": "string" } }, "required": [ "text", "url" ], "additionalProperties": {} }, { "type": "null" } ] } }, "required": [ "clientId", "weekStart", "weekEnd", "daysLogged", "compliancePct", "averages", "targets", "bestDay", "worstDay", "topFoods", "photosLogged", "comments", "partialNutrients", "attribution" ], "additionalProperties": false }GET
/api/nutrition/summaryA day's nutrition totals against the client's goals
Response 200
NutritionDaySummaryResponse{ "type": "object", "properties": { "clientId": { "type": "string" }, "date": { "type": "string", "description": "Calendar day, YYYY-MM-DD" }, "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" }, "fiber": { "type": "number" }, "entryCount": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "goals": { "type": "object", "properties": { "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" } }, "required": [ "calories", "protein", "carbs", "fat" ], "additionalProperties": {} }, "targetSource": { "type": "string", "enum": [ "override", "dayType", "set", "onboarding", "default" ], "description": "Where the goals come from; treat an unknown value as \"set\"" }, "dayType": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "meals": { "type": "object", "properties": { "breakfast": { "type": "object", "properties": { "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" }, "fiber": { "type": "number" }, "sugar": { "type": "number" }, "saturatedFat": { "type": "number" }, "sodiumMg": { "type": "number" }, "entryCount": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "partial": { "type": "boolean", "description": "An entry in the meal leaves sugar, saturated fat or sodium unknown" } }, "required": [ "calories", "protein", "carbs", "fat", "fiber", "sugar", "saturatedFat", "sodiumMg", "entryCount", "partial" ], "additionalProperties": false }, "lunch": { "type": "object", "properties": { "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" }, "fiber": { "type": "number" }, "sugar": { "type": "number" }, "saturatedFat": { "type": "number" }, "sodiumMg": { "type": "number" }, "entryCount": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "partial": { "type": "boolean", "description": "An entry in the meal leaves sugar, saturated fat or sodium unknown" } }, "required": [ "calories", "protein", "carbs", "fat", "fiber", "sugar", "saturatedFat", "sodiumMg", "entryCount", "partial" ], "additionalProperties": false }, "dinner": { "type": "object", "properties": { "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" }, "fiber": { "type": "number" }, "sugar": { "type": "number" }, "saturatedFat": { "type": "number" }, "sodiumMg": { "type": "number" }, "entryCount": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "partial": { "type": "boolean", "description": "An entry in the meal leaves sugar, saturated fat or sodium unknown" } }, "required": [ "calories", "protein", "carbs", "fat", "fiber", "sugar", "saturatedFat", "sodiumMg", "entryCount", "partial" ], "additionalProperties": false }, "snack": { "type": "object", "properties": { "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" }, "fiber": { "type": "number" }, "sugar": { "type": "number" }, "saturatedFat": { "type": "number" }, "sodiumMg": { "type": "number" }, "entryCount": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "partial": { "type": "boolean", "description": "An entry in the meal leaves sugar, saturated fat or sodium unknown" } }, "required": [ "calories", "protein", "carbs", "fat", "fiber", "sugar", "saturatedFat", "sodiumMg", "entryCount", "partial" ], "additionalProperties": false } }, "required": [ "breakfast", "lunch", "dinner", "snack" ], "additionalProperties": false }, "extras": { "type": "object", "properties": { "sugar": { "type": "number" }, "saturatedFat": { "type": "number" }, "sodiumMg": { "type": "number" }, "partialNutrients": { "type": "array", "items": { "type": "string", "enum": [ "sugar", "sodium", "saturatedFat" ] } } }, "required": [ "sugar", "saturatedFat", "sodiumMg", "partialNutrients" ], "additionalProperties": false }, "remaining": { "type": "object", "properties": { "calories": { "type": "number" }, "protein": { "type": "number" }, "carbs": { "type": "number" }, "fat": { "type": "number" } }, "required": [ "calories", "protein", "carbs", "fat" ], "additionalProperties": false, "description": "Goal minus eaten; negative when over" }, "water": { "type": "object", "properties": { "ml": { "type": "number" }, "goalMl": { "type": "number" } }, "required": [ "ml", "goalMl" ], "additionalProperties": false } }, "required": [ "clientId", "date", "calories", "protein", "carbs", "fat", "fiber", "entryCount", "goals", "targetSource", "dayType", "meals", "extras", "remaining", "water" ], "additionalProperties": false }PUT
/api/nutrition/targets/overridesUse one day type, or one calorie figure, on one date only
Request body
NutritionTargetOverride{ "type": "object", "properties": { "date": { "type": "string" }, "dayTypeId": { "anyOf": [ { "type": "string", "pattern": "^[cC][^\\s-]{8,}$" }, { "type": "null" } ] }, "calories": { "anyOf": [ { "type": "integer", "minimum": 500, "maximum": 10000 }, { "type": "null" } ] } }, "required": [ "date", "dayTypeId" ], "additionalProperties": false }DELETE
/api/nutrition/targets/overridesRemove a one-date override: the date follows the week again
POST
/api/recipes/{id}/logLog servings of a recipe the client may use, with the items' summed micros
Request body
RecipeLog{ "type": "object", "properties": { "clientId": { "type": "string" }, "date": { "type": "string" }, "mealType": { "type": "string", "enum": [ "breakfast", "lunch", "dinner", "snack" ] }, "servings": { "type": "number", "minimum": 0.25, "maximum": 50 }, "loggedAt": { "anyOf": [ { "type": "string", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" }, { "type": "null" } ] }, "photoUrl": { "anyOf": [ { "type": "string", "minLength": 1, "maxLength": 2048 }, { "type": "null" } ] } }, "required": [ "date", "mealType" ], "additionalProperties": false }POST
/api/recipes/{id}/save-as-foodSave one serving of a recipe as one of the caller's own foods
GET
/api/users/me/dietaryThe caller's allergens and diets, which the food screens flag
PUT
/api/users/me/dietaryReplace the caller's allergens and diets (from the fixed lists)
Request body
DietaryProfile{ "type": "object", "properties": { "allergens": { "maxItems": 14, "type": "array", "items": { "type": "string", "enum": [ "gluten", "milk", "eggs", "nuts", "peanuts", "sesame-seeds", "soybeans", "fish", "crustaceans", "molluscs", "celery", "mustard", "lupin", "sulphur-dioxide-and-sulphites" ] } }, "diet": { "maxItems": 9, "type": "array", "items": { "type": "string", "enum": [ "vegetarian", "vegan", "pescatarian", "halal", "kosher", "gluten_free", "lactose_free", "low_carb", "keto" ] } } }, "required": [ "allergens", "diet" ], "additionalProperties": false }POST
/api/waterSet or add to a day's water: exactly one of `glasses`, `ml` (the day's total) or `addMl` (added in one atomic step; negative takes back)
Request body
WaterUpsert{ "type": "object", "properties": { "clientId": { "type": "string" }, "date": { "type": "string" }, "glasses": { "type": "integer", "minimum": 0, "maximum": 32 }, "ml": { "type": "integer", "minimum": 0, "maximum": 20000 }, "addMl": { "type": "integer", "minimum": -5000, "maximum": 5000 } }, "required": [ "date" ], "additionalProperties": false }
Comments
GET
/api/log-comments/countsComments per thread for one target type on one day: how many, and how many the caller has not read
Exports
GET
/api/exports/coachThe coach's recent whole-practice exports, with download links for ready parts. Start one with POST (second factor required, not part of this reference)
Response 200
CoachExportJobList{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "status": { "type": "string", "enum": [ "queued", "running", "ready", "failed" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "finishedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] }, "expiresAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ], "description": "After this the parts are no longer offered" }, "parts": { "type": "array", "items": { "type": "object", "properties": { "part": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, "description": "1-based part number" }, "bytes": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "files": { "type": "array", "items": { "type": "string" }, "description": "File names in this ZIP, besides manifest.json" }, "url": { "description": "15-minute download link; null unless ready", "type": [ "string", "null" ] } }, "required": [ "part", "bytes", "files", "url" ], "additionalProperties": {} } } }, "required": [ "id", "status", "createdAt", "finishedAt", "expiresAt", "parts" ], "additionalProperties": {} }, "description": "The five most recent exports, newest first" }, "nextCursor": { "type": "null" } }, "required": [ "items", "nextCursor" ], "additionalProperties": false }GET
/api/exports/coach/{id}One of the caller's whole-practice exports
Response 200
CoachExportJob{ "type": "object", "properties": { "id": { "type": "string" }, "status": { "type": "string", "enum": [ "queued", "running", "ready", "failed" ] }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" }, "finishedAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ] }, "expiresAt": { "anyOf": [ { "type": "string", "description": "ISO-8601 timestamp" }, { "type": "null" } ], "description": "After this the parts are no longer offered" }, "parts": { "type": "array", "items": { "type": "object", "properties": { "part": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, "description": "1-based part number" }, "bytes": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "files": { "type": "array", "items": { "type": "string" }, "description": "File names in this ZIP, besides manifest.json" }, "url": { "description": "15-minute download link; null unless ready", "type": [ "string", "null" ] } }, "required": [ "part", "bytes", "files", "url" ], "additionalProperties": {} } } }, "required": [ "id", "status", "createdAt", "finishedAt", "expiresAt", "parts" ], "additionalProperties": {} }
Studios
GET
/api/tenants/{id}/auditThe studio's audit log, for its owner only
Response 200
StudioAuditPage{ "type": "object", "properties": { "items": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "action": { "type": "string" }, "actor": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, { "type": "null" } ] }, "targetType": { "type": [ "string", "null" ] }, "targetId": { "type": [ "string", "null" ] }, "metadata": {}, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" } }, "required": [ "id", "action", "actor", "targetType", "targetId", "metadata", "createdAt" ], "additionalProperties": {} } }, "nextCursor": { "description": "Pass as `?cursor=` for the next page", "type": [ "string", "null" ] }, "staff": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false }, "description": "The studio's coaches, for the actor filter" } }, "required": [ "items", "nextCursor", "staff" ], "additionalProperties": false }
Feedback
GET
/api/npsWhether the NPS prompt is on and the caller is due it
Response 200
NpsState{ "type": "object", "properties": { "enabled": { "type": "boolean", "description": "NPS_ENABLED is on" }, "eligible": { "type": "boolean", "description": "The caller is due the prompt now" } }, "required": [ "enabled", "eligible" ], "additionalProperties": false }POST
/api/npsAnswer the NPS prompt (0-10 and an optional comment) or dismiss it
Request body
NpsSubmit{ "anyOf": [ { "type": "object", "properties": { "score": { "type": "integer", "minimum": 0, "maximum": 10 }, "comment": { "type": "string", "maxLength": 1000 } }, "required": [ "score" ], "additionalProperties": false }, { "type": "object", "properties": { "dismissed": { "type": "boolean", "const": true } }, "required": [ "dismissed" ], "additionalProperties": false } ] }Response 201
NpsResponseCreated{ "type": "object", "properties": { "id": { "type": "string" }, "score": { "anyOf": [ { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, { "type": "null" } ], "description": "null for a dismissal" }, "createdAt": { "type": "string", "description": "ISO-8601 timestamp" } }, "required": [ "id", "score", "createdAt" ], "additionalProperties": {} }
Referrals
GET
/api/referralsThe coach's referral code, link and ledger totals
Response 200
ReferralSummary{ "type": "object", "properties": { "code": { "type": "string", "description": "The coach referral code, 8 characters" }, "link": { "type": "string", "description": "Sign-up link carrying the code" }, "referred": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991, "description": "Coaches who signed up with it" }, "rewards": { "type": "array", "items": { "type": "object", "properties": { "amountMinor": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, "currency": { "type": "string" } }, "required": [ "amountMinor", "currency" ], "additionalProperties": false }, "description": "Ledger totals per currency; 0 unless a reward is configured" } }, "required": [ "code", "link", "referred", "rewards" ], "additionalProperties": false }
Webhooks
Add an endpoint under Settings. NetFit sends a signed POST request with a JSON body when one of these events happens. Payloads carry ids and timestamps only: read the details through the API.
Events
workout.completedWorkout completedPayload
{ "type": "object", "properties": { "id": { "type": "string", "description": "Delivery id; the same as the NetFit-Delivery header" }, "event": { "type": "string", "const": "workout.completed" }, "createdAt": { "type": "string", "description": "ISO-8601 time the event was queued" }, "data": { "type": "object", "properties": { "assignedWorkoutId": { "type": "string" }, "workoutLogId": { "description": "null when the session was marked done without logging sets", "type": [ "string", "null" ] }, "clientId": { "type": "string" }, "completedAt": { "type": "string" } }, "required": [ "assignedWorkoutId", "workoutLogId", "clientId", "completedAt" ], "additionalProperties": false } }, "required": [ "id", "event", "createdAt", "data" ], "additionalProperties": false }checkin.submittedCheck-in submittedPayload
{ "type": "object", "properties": { "id": { "type": "string", "description": "Delivery id; the same as the NetFit-Delivery header" }, "event": { "type": "string", "const": "checkin.submitted" }, "createdAt": { "type": "string", "description": "ISO-8601 time the event was queued" }, "data": { "type": "object", "properties": { "responseId": { "type": "string" }, "formId": { "type": "string" }, "clientId": { "type": "string" }, "submittedAt": { "type": "string" } }, "required": [ "responseId", "formId", "clientId", "submittedAt" ], "additionalProperties": false } }, "required": [ "id", "event", "createdAt", "data" ], "additionalProperties": false }booking.createdSession bookedPayload
{ "type": "object", "properties": { "id": { "type": "string", "description": "Delivery id; the same as the NetFit-Delivery header" }, "event": { "type": "string", "const": "booking.created" }, "createdAt": { "type": "string", "description": "ISO-8601 time the event was queued" }, "data": { "type": "object", "properties": { "bookingId": { "type": "string" }, "clientId": { "type": [ "string", "null" ] }, "startsAt": { "type": "string" } }, "required": [ "bookingId", "clientId", "startsAt" ], "additionalProperties": false } }, "required": [ "id", "event", "createdAt", "data" ], "additionalProperties": false }booking.cancelledSession cancelledPayload
{ "type": "object", "properties": { "id": { "type": "string", "description": "Delivery id; the same as the NetFit-Delivery header" }, "event": { "type": "string", "const": "booking.cancelled" }, "createdAt": { "type": "string", "description": "ISO-8601 time the event was queued" }, "data": { "type": "object", "properties": { "bookingId": { "type": "string" }, "clientId": { "type": [ "string", "null" ] }, "startsAt": { "type": "string" } }, "required": [ "bookingId", "clientId", "startsAt" ], "additionalProperties": false } }, "required": [ "id", "event", "createdAt", "data" ], "additionalProperties": false }client.joinedClient joinedPayload
{ "type": "object", "properties": { "id": { "type": "string", "description": "Delivery id; the same as the NetFit-Delivery header" }, "event": { "type": "string", "const": "client.joined" }, "createdAt": { "type": "string", "description": "ISO-8601 time the event was queued" }, "data": { "type": "object", "properties": { "clientId": { "type": "string" }, "joinedAt": { "type": "string" } }, "required": [ "clientId", "joinedAt" ], "additionalProperties": false } }, "required": [ "id", "event", "createdAt", "data" ], "additionalProperties": false }pingTestPayload
{ "type": "object", "properties": { "id": { "type": "string", "description": "Delivery id; the same as the NetFit-Delivery header" }, "event": { "type": "string", "const": "ping" }, "createdAt": { "type": "string", "description": "ISO-8601 time the event was queued" }, "data": { "type": "object", "properties": { "endpointId": { "type": "string" }, "sentAt": { "type": "string" } }, "required": [ "endpointId", "sentAt" ], "additionalProperties": false } }, "required": [ "id", "event", "createdAt", "data" ], "additionalProperties": false }
Verify each delivery
- Read the NetFit-Signature header. It has the form t=TIMESTAMP,v1=SIGNATURE, with TIMESTAMP in Unix seconds.
- Compute the HMAC-SHA256 of TIMESTAMP.BODY (the timestamp, a full stop, then the raw body) with the endpoint's secret, before you parse the body.
- Compare the hex result with SIGNATURE in constant time, and reject a TIMESTAMP more than five minutes away from your clock.
- Answer with a 2xx status within 10 seconds. Anything else is retried after 1 min, 5 min, 30 min and 2 h. The NetFit-Delivery header stays the same on every retry, so use it to skip duplicates.
Secret rotation
After an endpoint’s secret is rotated, each request carries two v1 signatures for 24 hours, one per secret. Accept the request when any v1 matches.
Failures, replays and switch-off
A delivery that still fails after five attempts is marked failed. After 10 failed deliveries in a row the endpoint is switched off and its owner is notified; switching it back on sends the queued events. Any finished delivery can be resent from the delivery log, and the copy has a new NetFit-Delivery id.
Example in Node.js
import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody: the request body exactly as received, before JSON.parse.
export function verifyNetFitWebhook(rawBody, header, secret, toleranceSeconds = 300) {
let timestamp = NaN
const signatures = []
for (const part of (header ?? '').split(',')) {
const [key, value] = part.trim().split('=')
if (key === 't') timestamp = Number(value)
if (key === 'v1' && value) signatures.push(value)
}
if (!Number.isInteger(timestamp) || signatures.length === 0) return false
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false
const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest()
// Any v1 may match: after a rotation there is one per secret.
return signatures.some((signature) => {
const given = Buffer.from(signature, 'hex')
return given.length === expected.length && timingSafeEqual(given, expected)
})
}