A versioned, read-only HTTP API that lets a server-side integration you control pull your own Eclipse session data — sessions, processing status, rep/window-level EMG & IMU features, AI reports, and file metadata/downloads.
Server-side only. API keys are personal access tokens with the same power as a password for your data. Never embed a key in a browser page, mobile app bundle, or any client distributed to others — anyone who extracts it can read all of your session data until you revoke it. Call this API from your own backend, script, or CI job, and forward only the results you need to any client you build.
Getting a key
- Sign in to the portal and go to Account Settings.
- In the API Keys section, click Create New Key.
- Name the key, choose the scopes it needs, choose an expiry (default 90 days, maximum 365), and confirm with your password (and authenticator code, if you have two-factor authentication enabled).
- Copy the plaintext key immediately — it is shown exactly once and cannot be retrieved again. If you lose it, revoke it and create a new one.
You can have at most 10 active (non-revoked, non-expired) keys at a time. Revoking a key is immediate and does not require your password.
Authentication
Send the key as a bearer token on every request:
Authorization: Bearer ecl_live_v1_<random>
Requests without a valid, unexpired, unrevoked key are rejected with 401 unauthorized. There is no cookie- or session-based authentication for /api/v1 — it is exclusively for bearer-token clients.
Scopes
Each key is created with one or more scopes. A request fails with 403 forbidden if the key doesn't have the scope an endpoint requires.
| Scope | Grants |
|---|---|
sessions:read | List sessions; read session metadata and processing status |
features:read | Read rep- and window-level EMG/IMU features |
reports:read | Read the latest completed AI-generated report |
files:read | List uploaded file metadata; mint short-lived download links |
Keys never inherit coach, organization-admin, or session-share access — a key can only ever read the data owned by the account that created it, regardless of scope.
Response envelope
Successful responses:
{ "data": /* object or array */, "meta": { "nextCursor": "...", "hasMore": true } }
meta is only present on paginated list endpoints (and carries extra fields on the features endpoint — see below).
Errors:
{ "error": { "code": "not_found", "message": "..." } }
code is one of unauthorized, forbidden, not_found, bad_request, rate_limited, internal_error. Error messages never include database details, storage paths, or other internal identifiers. An ID that doesn't exist and an ID that belongs to another user both return the same 404 not_found — the API never reveals whether a resource exists for someone else.
Every response includes Cache-Control: no-store and X-Request-Id; include the latter when reporting an issue.
Errors
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | Malformed query parameter, body, or cursor |
| 401 | unauthorized | Missing, malformed, unknown, revoked, or expired key |
| 403 | forbidden | Key is valid but lacks the required scope |
| 404 | not_found | Resource doesn't exist, or isn't owned by this key |
| 429 | rate_limited | Per-key rate limit exceeded — see Retry-After |
| 500 | internal_error | Unexpected server error |
Rate limits
Each key is limited to 60 requests/minute, enforced independently per key. Every response carries:
RateLimit-Limit: 60
RateLimit-Remaining: <n>
A 429 additionally includes Retry-After: <seconds>. Limits reset on a fixed one-minute window per key; there is no cross-key or account-wide pooling.
Pagination
List endpoints return an opaque nextCursor string — treat it as a black box; don't construct or parse it yourself. Pass it back as the cursor query parameter to get the next page. hasMore tells you whether another page exists.
- Default page size is 100 items; pass
limit(1–500) to change it. GET /api/v1/sessionsalso acceptsupdated_after(ISO-8601) to fetch only sessions changed since a point in time, ordered on the same(updated_at, id)axis as the cursor — safe to use for incremental sync. Note thatupdated_atonly advances on session-record edits (e.g. renaming a session), not when it's reprocessed or gets a new report, since those touch child tables, not the session row itself.GET /api/v1/sessions/:id/featurespins pagination to one processing run: the first page selects the most recently completed (or partial) run and returns its ID inmeta.processedSessionId; every subsequent page for that cursor stays pinned to that same run, so a reprocessing job started mid-sync can never mix rows from two different runs into one paginated result.- A cursor that fails to decode returns
400 bad_requestrather than silently starting over.
Endpoints
All paths are relative to https://www.eclipseperf.com.
GET /api/v1/sessions
Requires sessions:read. Lists your sessions, ordered oldest-to-newest by (updated_at, id).
Query parameters: cursor, limit, updated_after.
{
"data": [
{
"id": "uuid",
"name": "Morning Training",
"description": null,
"workoutType": "Squats",
"startedAt": "2026-01-15T08:00:00.000Z",
"endedAt": "2026-01-15T08:30:00.000Z",
"createdAt": "2026-01-15T08:31:00.000Z",
"updatedAt": "2026-01-15T08:31:00.000Z"
}
],
"meta": { "nextCursor": "eyJ1IjoiMjAyNi...", "hasMore": true }
}
GET /api/v1/sessions/:id
Requires sessions:read. Returns session metadata plus the latest processing run summary (or null if the session hasn't been processed yet).
{
"data": {
"id": "uuid",
"name": "Morning Training",
"description": null,
"workoutType": "Squats",
"startedAt": "2026-01-15T08:00:00.000Z",
"endedAt": "2026-01-15T08:30:00.000Z",
"createdAt": "2026-01-15T08:31:00.000Z",
"updatedAt": "2026-01-15T08:31:00.000Z",
"processing": {
"id": "uuid",
"pipelineVersionId": "uuid",
"status": "completed",
"startedAt": "2026-01-15T08:31:05.000Z",
"completedAt": "2026-01-15T08:31:40.000Z",
"durationMs": 1800000,
"emgSampleCount": 540000,
"imuSampleCount": 108000,
"bleQuality": { "...": "..." },
"qualityFlags": { "...": "..." },
"createdAt": "2026-01-15T08:31:05.000Z",
"updatedAt": "2026-01-15T08:31:40.000Z"
}
}
}
GET /api/v1/sessions/:id/features
Requires features:read. Returns rep- or window-level features for the session's latest completed/partial processing run.
Query parameters: level (required — windows or reps), cursor, limit.
{
"data": [
{
"windowIndex": 0,
"startTimeMs": 0,
"endTimeMs": 500,
"emgRms": [0.12, 0.31, 0.28, 0.09],
"emgEnvelope": [0.10, 0.27, 0.24, 0.08],
"emgMedianFreq": [88.2, 94.1, 91.7, 85.3],
"imuMagnitudeMean": 1.02,
"imuIntensity": 0.44,
"extra": null,
"createdAt": "2026-01-15T08:31:40.000Z"
}
],
"meta": {
"nextCursor": "eyJwIjoiMTIzNC...",
"hasMore": true,
"processedSessionId": "uuid",
"level": "windows"
}
}
emgRms, emgEnvelope, emgMedianFreq, etc. are 4-element arrays indexed by EMG channel (0 = Biceps Femoris / Outer Hamstring, 1 = Vastus Lateralis / Outer Quad, 2 = Vastus Medialis / Inner Quad, 3 = Semitendinosus / Inner Hamstring). Any element may be null when that channel's data was absent or low-quality for that window/rep. level=reps additionally includes durationMs, emgMeanRectified, emgOnsetMs, emgOffsetMs, emgEmdMs, imuPeakMagnitude, and phaseData.
If the session has no completed or partial processing run yet, data is [], meta.processedSessionId is null, and meta.hasMore is false.
GET /api/v1/sessions/:id/report
Requires reports:read. Returns the latest completed AI report for the session.
Query parameters: type — simple (session_analysis, default). detailed is reserved for Eclipse-internal use and always returns 404 not_found for external API keys, identical to the response when no report of that type exists.
{
"data": {
"id": "uuid",
"reportType": "session_analysis",
"status": "completed",
"content": { "dataAnalysis": { "...": "..." }, "narrative": { "...": "..." }, "metadata": { "...": "..." } },
"generatedAt": "2026-01-15T08:32:00.000Z",
"createdAt": "2026-01-15T08:32:00.000Z",
"updatedAt": "2026-01-15T08:32:00.000Z"
}
}
Returns 404 not_found if no completed report of that type exists yet (including while one is still generating). content is never null for a completed report.
GET /api/v1/sessions/:id/files
Requires files:read. Lists uploaded file metadata — never a storage path.
{
"data": [
{
"id": "uuid",
"fileName": "left-sleeve.eclbin",
"fileSize": 1048576,
"fileType": "application/octet-stream",
"leg": "left",
"uploadedAt": "2026-01-15T08:05:00.000Z"
}
]
}
GET /api/v1/sessions/:id/files/:fileId/download
Requires files:read. Mints a 60-second signed URL for downloading the raw file from storage.
{ "data": { "url": "https://.../session-files/...", "expiresInSeconds": 60 } }
This is a GET, not a POST, because minting a short-lived URL has no persistent side effect on your data — it keeps the whole /api/v1 surface read-only. Each call writes a dedicated, synchronous audit record before the URL is returned; if the signed URL can't be created or that audit write fails, the endpoint returns 500 internal_error and never includes a URL in the response body. Because this issuance audit is separate from the general per-request usage event every route writes, a single successful download call produces two usage-event rows by design.
Revoking a key does not retroactively invalidate a signed URL that was already issued. The URL keeps working for the remainder of its (at most 60-second) lifetime even after revocation — treat any URL you receive as sensitive for that short window.
Key rotation & versioning
Every token embeds a version, e.g. ecl_live_v1_<random>. If Eclipse rotates the server-side signing secret, new keys are issued under a new version (ecl_live_v2_...) while existing v1 keys keep working until they individually expire or are revoked — you do not need to take any action when a rotation happens. Your key's prefix (e.g. ecl_live_v1_AbCdEfGh) is shown in Account Settings so you can identify which key is which without ever seeing the full secret again.
curl examples
# List sessions
curl -H "Authorization: Bearer ecl_live_v1_..." \
"https://www.eclipseperf.com/api/v1/sessions?limit=20"
# Incremental sync since a point in time
curl -H "Authorization: Bearer ecl_live_v1_..." \
"https://www.eclipseperf.com/api/v1/sessions?updated_after=2026-01-01T00:00:00Z"
# Session detail
curl -H "Authorization: Bearer ecl_live_v1_..." \
"https://www.eclipseperf.com/api/v1/sessions/<session-id>"
# Window-level features, first page
curl -H "Authorization: Bearer ecl_live_v1_..." \
"https://www.eclipseperf.com/api/v1/sessions/<session-id>/features?level=windows"
# Latest report
curl -H "Authorization: Bearer ecl_live_v1_..." \
"https://www.eclipseperf.com/api/v1/sessions/<session-id>/report"
# File metadata, then a download link
curl -H "Authorization: Bearer ecl_live_v1_..." \
"https://www.eclipseperf.com/api/v1/sessions/<session-id>/files"
curl -H "Authorization: Bearer ecl_live_v1_..." \
"https://www.eclipseperf.com/api/v1/sessions/<session-id>/files/<file-id>/download"
Support
Questions or suspect a leaked key? Revoke it immediately from Account Settings, then contact support with the X-Request-Id from any affected response.