Platform
Rate limits
The free tier is per app and costs nothing. Every response tells you where you stand, so you never have to guess.
The free tier
| Limit | Per 15 minutes | Per day |
|---|---|---|
| Your app, across all its athletes and tokens | 100 | 10,000 |
| One athlete within your app | 30 | 2,000 |
These count requests to the Bearer API: GET /v1/profile, GET / and GET /. Token exchange, refresh, revocation and webhooks don't count. A request that is turned away with 429 doesn't count either.
The per-athlete limit stops one busy account from using your whole budget. If we raise your app's limits, the per-athlete limit grows with them, to 30% of the 15-minute limit and 20% of the daily one, and never below 30 and 2,000 unless the app limit itself is lower.
Need more?
Email [email protected] with your client_id, what you're building and the volume you expect. There is no form in the console yet. Once raised, GET / shows "tier": "custom" and the new numbers. There is no charge.
Headers on Bearer responses
Every Bearer response that gets past the token check carries these, including 403, 404 and 429 rate_. A 401 invalid_ and a 429 service_ don't, because neither is counted against your app.
| Header | Meaning |
|---|---|
RateLimit-Policy | Your app's windows, for example 100;w=900, |
RateLimit-Limit, RateLimit-Remaining | The app window closest to running out |
RateLimit-Reset | Seconds until that window resets |
X-RateLimit-Limit, X-RateLimit-Remaining | The same window, older header names |
X-RateLimit-Reset | When that window resets, as Unix seconds |
X-RateLimit-User-Limit, X-RateLimit-User-Remaining, X-RateLimit-User-Reset | This athlete's window closest to running out; reset in Unix seconds |
The RateLimit-* headers follow the IETF HTTP API rate-limit draft. Note the two reset styles: RateLimit-Reset is a number of seconds, X-RateLimit-Reset is a timestamp.
When you hit a limit
You get 429 rate_ with Retry-After in seconds:
{
"code": "rate_limit_exceeded",
"message": "This app has used its request limit for this athlete; retry after 37 seconds.",
"request_id": "req_...",
"details": {"scope": "athlete", "per_15_minutes": 100, "per_day": 10000, "per_athlete_per_15_minutes": 30, "per_athlete_per_day": 2000}
}details.scope is app when your whole app ran out and athlete when one athlete did. Pause the affected work until Retry-After has passed. For athlete, other athletes' requests still go through.
The shared ceiling
All third-party apps together share a ceiling of 50 requests per second, with bursts to 100. When it's reached you get 429 service_ with Retry-After. It keeps response times steady for everyone, and it isn't counted against your app.
Token endpoint
POST / counts failures, not successes, so a stranger who knows your public client_id can't lock out your real traffic:
| What | Limit per minute | Answer |
|---|---|---|
| Failed requests from one address for one app | 20 | 429 slow_down |
Requests with a malformed client_id from one address | 60 | 429 slow_down |
| Successful exchanges and refreshes for one app | 3,000 | 429 slow_down |
| Any token requests from one address | 600 | 429 slow_down |
Failures across all addresses for one app (up to 600 a minute) are counted for our alerting but never block a valid request. The check and the count are separate steps, so a burst of parallel requests can pass a limit by at most the number in flight.
Other limits
| What | Limit |
|---|---|
| Athlete approvals (consent submissions) | 60 an hour per athlete |
| Account sign-ups | 5 an hour per network address |
| Sign-in attempts | 10 per 15 minutes per network address |
| App registrations | 20 an hour per account, 60 an hour per network address |
| Apps per account | 10 |
| Webhook subscriptions per app | 3 |
| Workout file uploads | 20 an hour per athlete |