API for Companies
Request API access
Connect your company’s product to send completed workouts, sleep and daily measurements to an athlete’s EffortSignal account. This guide covers OAuth connections for your product’s users. We register products manually. Each athlete then chooses whether to connect your product and which categories it may send.
For a script that works only with your own account, see Personal API key.
This opens an email draft; review and send it to submit your request. If your email application does not open, email hello@effortsignal.com with subject EffortSignal API access and copy this template:
Product name:
Website:
Responsible contact and company-domain email:
Client type (server or public app):
Callback URLs (exact HTTPS addresses):
Data categories (activities, sleep, health):
Expected athletes and uploads per day:
Historical data needed:
Test or production:
Include only product and contact details. Keep athlete data, passwords, secrets and tokens out of the request. For a native application’s local callback, specify the literal loopback address (http://127.0.0.1:PORT/callback or http://[::1]:PORT/callback). Remote callbacks require HTTPS; wildcards and arbitrary forwarding URLs are not accepted.
Registration and credentials · Working example · OAuth · Send data · Limits and retries
Registration and credentials
We verify the responsible contact and callback URLs, confirm the intended data categories and register your product. Use separate registrations for test and production environments. There is no developer account to create.
You receive a client_id, the registered callbacks, client type, environment addresses and this guide. A product with its own backend also receives a client_secret through a recipient-restricted, expiring protected share or an agreed encrypted channel. We do not email the raw secret. Confirm receipt before the share expires. Contact us through the same verified contact if delivery or replacement is needed.
Store a confidential secret on your server in your secret manager. A mobile application or browser application is a public client and receives no secret. Both client types use Authorization Code with PKCE S256. A product registration identifies your application; OAuth tokens identify each consenting athlete. An athlete’s password is entered only on EffortSignal.
| Production setting | Value |
|---|---|
| Issuer | https://app.effortsignal.com |
| Authorization | https://app.effortsignal.com/oauth/authorize |
| Token exchange and refresh | https://app.effortsignal.com/oauth/token |
| Token revocation | https://app.effortsignal.com/oauth/revoke |
| Ingest resource | https://app.effortsignal.com/api/v1/ingest (no trailing slash) |
| Request schemas | OpenAPI |
Discover OAuth metadata at /.well-known/oauth-authorization-server with query parameter resource=https://app.effortsignal.com/api/v1/ingest. Keep the advertised issuer and validate the authorization response’s iss. Ingest uses separate permissions from the AI/MCP connection.
Working example
Download the Python quickstart. It uses Python 3.11 or newer and its standard library. Read the script before running it. Prepare your registered client ID, exact callback and, for a server client, your secret.
Use your own test EffortSignal athlete account: the example saves an artificial 30-minute cycling recording and yesterday’s synthetic sleep/resting-pulse measurements. Keep it separate from your real training history. Your callback must capture the complete authorization response without redeeming the code itself; this example performs the exchange. For testing, configure a callback that displays the response URL without logging it or loading third-party analytics.
python3 ingest_quickstart.py \
--client-id YOUR_CLIENT_ID \
--redirect-uri https://your-product.example/oauth/effortsignal/callback
Public clients add --public. Server clients enter their secret at a hidden prompt. Open the printed authorization URL, sign in to the test account, review the requested categories and choose Allow. Paste the complete callback URL at the second hidden prompt within 60 seconds. Cancel returns access_denied and sends no data.
The script validates state/issuer, exchanges the code, reads the connection timezone, uploads both examples, refreshes the token and checks the import receipt for up to 60 seconds. It reports published separately from file acceptance. If processing is still pending, retain the import ID and check it later through the receipt endpoint. Add --disconnect to revoke the test connection after these checks; imported data remains saved. Access and refresh tokens stay in memory and are never printed or written to disk.
The example is a single test run. In your product, save each athlete’s tokens in protected server storage, replace rotated refresh tokens atomically and keep a durable record of source IDs, revision timestamps and accepted import receipts.
OAuth
Your product’s Connect EffortSignal action opens the authorization endpoint with:
client_id=YOUR_CLIENT_ID
redirect_uri=YOUR_EXACT_REGISTERED_CALLBACK
response_type=code
resource=https://app.effortsignal.com/api/v1/ingest
scope=activities:write sleep:write health:write
state=UNPREDICTABLE_PER_ATTEMPT_VALUE
code_challenge=BASE64URL_SHA256_OF_YOUR_RANDOM_VERIFIER
code_challenge_method=S256
URL-encode the parameters. Request only the categories your product sends: activities:write for completed recordings, sleep:write for sleep and naps, and health:write for other daily measurements. The quickstart requests all three because it demonstrates both upload routes.
At your callback, verify state and iss before exchanging code. POST form-encoded grant_type=authorization_code, client_id, code, code_verifier, the original redirect_uri and resource to the token endpoint. A server client also authenticates with HTTP Basic (client_secret_basic); a public client uses no secret. Codes are single-use and expire after 60 seconds.
Use Authorization: Bearer ACCESS_TOKEN on ingest requests. Access tokens normally last 15 minutes; use the returned expires_in. Refresh with form-encoded grant_type=refresh_token, client_id, refresh_token and the same resource, plus HTTP Basic for server clients. Refresh tokens rotate and normally expire after 30 days. Serialize refreshes per athlete and retain the new token pair before continuing work. Expired/revoked grants require fresh consent.
Tokens can send only to their consenting athlete’s account. They cannot read the athlete’s ES profile, calendar or full training history. A request body cannot select an athlete or impersonate another source. Product registration alone grants no access to athlete data.
Send data
All paths below are relative to the ingest resource and require its bearer token.
| Method and path | Purpose |
|---|---|
GET /connection/ |
Read your connection ID, scopes, profile timezone, formats and upload limits |
POST /activities/ |
Upload a completed FIT, TCX, GPX or .eshealth recording |
GET /imports/{import_job_id}/ |
Read this application’s receipt and publication state |
POST /daily-health/ |
Send a batch of sleep/daily measurements |
DELETE /connection/ |
Revoke this connection and retain accepted history |
Completed recordings
POST the file bytes as application/octet-stream, with URL-encoded query parameters external_id, source_updated_at and filename. Use a stable ID for the same recording and an explicit-offset timestamp representing the source’s revision. The maximum file size is 64 MiB. Only .eshealth supports gzip; the decoded limit is 128 MiB.
If your product has no FIT/TCX/GPX file, send normalized .eshealth: UTF-8 NDJSON with one JSON object per line. A summary-only recording needs one workout line. This is the structure generated by the quickstart; replace the example’s values with your own source values:
{"record_type":"workout","schema_version":1,"workout_id":"example-ride-1","source_fingerprint":"7e8a1f0ba9e73c3e640eeb088b5b5b708e2698f8def17551d6729f116d71165a","activity_type":"cycling","sport":"cycling","started_at":"2026-10-20T09:00:00Z","ended_at":"2026-10-20T09:30:00Z","duration_seconds":1800,"moving_time_seconds":1800,"total_distance_meters":8000}
Set external_id=example-ride-1, filename=example-ride-1.eshealth and an appropriate source_updated_at. workout_id must equal external_id and contain at most 128 ASCII letters, digits, underscores or hyphens. The fingerprint is a stable 64-character lowercase SHA-256 source identity. Device metadata describes the actual measuring device; it does not grant a trusted-provider identity. Sparse session, lap, event, sample and route_point lines can follow. Do not manufacture sensor samples from a summary. Contact us with a synthetic format sample if your product needs a richer recording; never email real athlete data for format debugging.
A 202 response means the source file and durable processing work are saved. Store import_job_id and poll its receipt. state can be pending, running, succeeded, failed or needs_review. published=true confirms the activity is available; succeeded with published=false is still awaiting publication or review. A nonempty error describes a processing/publication failure.
Sleep and daily measurements
Read the exact profile timezone from /connection/. Send JSON with timezone and summaries, containing 1–31 unique local dates within a 31-day window. Each day needs local_date, a revision timestamp source_updated_at and provenance for exactly the measurements supplied. The quickstart builds dates in the returned timezone; this example shows the request shape:
{
"timezone": "Europe/Warsaw",
"summaries": [{
"local_date": "2026-10-20",
"source_updated_at": "2026-10-21T07:00:00Z",
"sleep_duration_seconds": 28800,
"resting_heart_rate_bpm": 55,
"metric_provenance": {
"sleep_duration_seconds": {"device": "Example watch", "measurement_method": "main_sleep"},
"resting_heart_rate_bpm": {"device": "Example watch", "measurement_method": "provider_resting_heart_rate"}
}
}]
}
Sleep/deep sleep/confirmed naps use seconds. Weight uses kg, SpO₂ uses percent, and HRV uses milliseconds. RMSSD and SDNN are distinct fields and must not be converted into each other. HRV also requires measured_at on that profile-local date and the matching nightly_rmssd, daytime_rmssd, manual_rmssd (or SDNN) method. Resting pulse must be a resting measurement. Missing fields remain unchanged; null/clearing values and unknown fields are rejected. See OpenAPI for all supported fields and bounds.
Limits and retries
Keep the same source ID, revision and bytes when retrying a recording. An identical accepted replay returns its original receipt with 200. Different bytes under the same revision or a stale new revision return 409. For a genuine correction, advance source_updated_at. Use a source revision clock; do not invent a fresh timestamp for each transport retry. Daily batches follow the same revision ordering, and unchanged replays do not trigger recalculation.
Future local dates and source clocks more than five minutes ahead are rejected. Never silently change a local date to make a timezone error disappear. Split history into valid batches and retain receipts. Uploading your own older history is supported through the same endpoints.
| Response | Action |
|---|---|
400 |
Correct the fields, units, provenance, callback or timezone; do not retry unchanged |
401 |
Refresh if applicable; revoked/expired consent requires reconnecting |
403 |
Check the granted data category; request fresh consent if more is needed |
404 |
Check the receipt belongs to this athlete and application |
409 |
Resolve the source revision conflict before sending again |
413 / 415 |
Reduce the body/batch or use a supported format/content type |
429 |
Respect Retry-After when present and back off |
Transport failure / 5xx |
Back off and retry with the same ID, revision and bytes |
In Settings → Integrations → Connected applications, athletes can inspect received-data dates and disconnect your product. Disconnect stops uploads and keeps imported history. Reconnecting preserves that source’s identity and invalidates its older grants. Automatic deletion synchronization, reading the full ES history and outbound webhooks are outside this API version.
For support, send the HTTP status, timestamp, endpoint and import ID to hello@effortsignal.com. Redact authorization headers, tokens, callback codes and athlete/file contents. Include a minimal synthetic request when needed.
Content checked October 21, 2026 · EffortSignal release 0.7.2