# API for Companies

Canonical HTML: https://effortsignal.com/developers/

Last content update: 2026-10-21

<!-- Keep partner onboarding on the existing contact channel; applications are registered manually
     and each athlete still grants access through OAuth. Examples never use a platform API key. -->
## 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](/personal-api-key/).

[Request API access](mailto:hello@effortsignal.com?subject=EffortSignal%20API%20access&body=Product%20name%3A%20%0AWebsite%3A%20%0AResponsible%20contact%20and%20company-domain%20email%3A%20%0AClient%20type%20%28server%20or%20public%20app%29%3A%20%0ACallback%20URLs%20%28exact%20HTTPS%20addresses%29%3A%20%0AData%20categories%20%28activities%2C%20sleep%2C%20health%29%3A%20%0AExpected%20athletes%20and%20uploads%20per%20day%3A%20%0AHistorical%20data%20needed%3A%20%0ATest%20or%20production%3A%20)

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:

```text
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](#registration-and-credentials) · [Working example](#working-example) · [OAuth](#oauth) · [Send data](#send-data) · [Limits and retries](#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](https://app.effortsignal.com/api/schema/) |

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](/examples/ingest_quickstart.py). 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.

```sh
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:

```text
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:

```json
{"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:

```json
{
  "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.
