Make your first request
- Subscribe to Pro in the telo iPhone app under Account → Plan, then sync your history.
- Open Account on the web dashboard and create a key for your project. Save it now: the full secret is shown only once.
- Set TELO_API_KEY in your server environment or local shell, then run the request below.
curl --fail-with-body 'https://telo.health/api/v1/list_metrics' \
-H "Authorization: Bearer $TELO_API_KEY"Authentication
Send Authorization: Bearer <key> on every request. Each key reads only its own account. Store it on your server or local machine, never in browser code, a public repository or a URL. Cookies and iPhone tokens do not authenticate this API.
Create up to five keys and revoke each project independently in Account. Revocation stays available after Pro expires. A paid Pro entitlement is required even for founder accounts; refunds and subscription changes follow the existing billing cache, up to 15 minutes, with webhook invalidation.
Manage API keysEndpoints
Base URL: https://telo.health/api/v1. Every endpoint uses GET and returns the same JSON payload as its MCP read tool, without the MCP envelope. Dates use YYYY-MM-DD. Lists use commas; booleans use true or false. Unknown or repeated parameters are rejected.
GET /data_status
Check coverage, import progress and the last sync before interpreting missing records.
- Required
None- Optional
None
curl --fail-with-body 'https://telo.health/api/v1/data_status' \
-H "Authorization: Bearer $TELO_API_KEY"GET /list_metrics
Discover the metric names, units and date ranges available in your account.
- Required
None- Optional
None
curl --fail-with-body 'https://telo.health/api/v1/list_metrics' \
-H "Authorization: Bearer $TELO_API_KEY"GET /query_daily
Read daily values or aggregate by week or month. Missing buckets are absent, not zero.
- Required
metrics, from, to- Optional
granularity=day|week|month
curl --fail-with-body 'https://telo.health/api/v1/query_daily?metrics=steps&from=2026-11-01&to=2026-11-08&granularity=day' \
-H "Authorization: Bearer $TELO_API_KEY"GET /sessions
List sleep nights or workouts. Workout filters and weekly grouping require kind=workout.
- Required
kind=sleep|workout, from, to- Optional
activity, granularity=session|week, max_hr
curl --fail-with-body 'https://telo.health/api/v1/sessions?kind=workout&from=2026-11-01&to=2026-11-08' \
-H "Authorization: Bearer $TELO_API_KEY"GET /workout_detail
Read a workout summary, events and available series. Use a UUID returned by sessions.
- Required
uuid- Optional
metrics, offset, event_offset, limit, max_hr, wall_tolerance_s
curl --fail-with-body 'https://telo.health/api/v1/workout_detail?uuid=WORKOUT_UUID&limit=500' \
-H "Authorization: Bearer $TELO_API_KEY"GET /derived_metrics
Read duration-based training load and personal HRV/resting-heart-rate baselines. Walking is excluded by default.
- Required
from, to- Optional
baseline_days=30..60 (60), include_walking=false, include_trimp=false, max_hr
curl --fail-with-body 'https://telo.health/api/v1/derived_metrics?from=2026-11-01&to=2026-11-08&include_trimp=true' \
-H "Authorization: Bearer $TELO_API_KEY"Code examples
The examples read the catalog only. Your API key belongs in the environment, not in the source file. HTTP failures must be handled before using the response.
Python
import json
import os
from urllib.request import Request, urlopen
request = Request(
"https://telo.health/api/v1/list_metrics",
headers={"Authorization": "Bearer " + os.environ["TELO_API_KEY"]},
)
# urlopen raises HTTPError for non-success responses.
with urlopen(request, timeout=30) as response:
catalog = json.load(response)
print(catalog)JavaScript (Node.js)
// Node.js, server-side only.
const response = await fetch("https://telo.health/api/v1/list_metrics", {
headers: { Authorization: `Bearer ${process.env.TELO_API_KEY}` },
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`telo HTTP ${response.status}`);
const catalog = await response.json();
console.log(catalog);Reading the response
Illustrative response for query_daily, using fictional data:
{
"rows": [
{
"period": "2026-11-01",
"metric": "steps",
"unit": "count",
"value": 8200,
"aggregation": "sum",
"days_present": 1,
"days_expected": 1,
"period_start": "2026-11-01",
"period_end": "2026-11-01",
"daily_min": 8200,
"daily_max": 8200
}
],
"truncated": false
}days_present counts days with stored values; days_expected describes the requested calendar coverage. Missing data and null are not zero. Preserve units and the local offset on timestamps. The API reads what the iPhone has synced; calling it does not trigger a sync. Workout summaries are permanent, while raw series are retained for 30 days. GPS is not included.
Limits and pagination
- 60 requests per minute per account, shared by all its keys. On 429, wait the number of seconds in Retry-After.
- At most 3,660 days per date query, or 366 for derived_metrics. query_daily accepts up to 256 comma-separated metric names.
- Date-based reads return up to 500 rows. If truncated is true, discard the partial response and query fewer metrics or smaller non-overlapping ranges aligned to complete weekly/monthly buckets. Do not treat a partial average as a complete period.
- workout_detail uses separate next_offset and next_event_offset values. Follow both independently until both are null. Defaults: offset=0, event_offset=0, limit=360; limit is at most 500. An empty metrics= returns metadata and events without series.
Errors
400- Invalid parameters, dates or range. Check the OpenAPI reference.
401- Missing, invalid or revoked personal key. Create a new key if necessary.
403- A paid Pro subscription is not active. Check Account → Plan on iPhone.
429- Rate limit reached. Wait Retry-After seconds before retrying.
500- Internal error. Retry later; do not interpret the response as empty health data.
503- Subscription verification is unavailable. Retry later.