Documentation
Best Practices
Guidelines for building reliable, secure, and performant Strava integrations.
Rate Limiting
Default Rate Limits
- 200overall requests per 15 minutes (2,000 per day)
- 100read (non-upload) requests per 15 minutes (1,000 per day)
Watch the read limit: you can hit the 100-per-15-minutes read ceiling well before the 200 overall limit. Approved apps can reach an upgraded tier of 400 per 15 minutes and 4,000 per day.
Requesting Higher Rate Limits
Start in the API Settings Dashboard. As of 2026, Standard Tier apps can self-upgrade there with no review, up to 10 connected athletes and higher rate limits. Bigger needs go through the Extended Access Tier review. Before requesting more:
- 1.Verify your app is actually hitting limits (check your API settings)
- 2.Optimize your API usage first. Use webhooks instead of polling, and throttle backfills
- 3.Review the API Agreement and Brand Guidelines
Reading Rate Limit Headers
Every API response includes headers showing your current usage:
| Header | Description |
|---|---|
| X-RateLimit-Limit | Maximum requests allowed (15min, daily) |
| X-RateLimit-Usage | Current usage (15min, daily) |
Handling Rate Limits
async function stravaRequest(endpoint, accessToken) {
const response = await fetch(`https://www.strava.com/api/v3${endpoint}`, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
// Check rate limits
const limitHeader = response.headers.get('X-RateLimit-Limit');
const usageHeader = response.headers.get('X-RateLimit-Usage');
if (limitHeader && usageHeader) {
const [limit15min, limitDaily] = limitHeader.split(',').map(Number);
const [usage15min, usageDaily] = usageHeader.split(',').map(Number);
console.log(`15min: ${usage15min}/${limit15min}, Daily: ${usageDaily}/${limitDaily}`);
// Warn if approaching limits
if (usage15min > limit15min * 0.8) {
console.warn('Approaching 15-minute rate limit');
}
}
// Handle 429 Too Many Requests
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 900;
console.error(`Rate limited. Retry after ${retryAfter} seconds`);
throw new Error('Rate limited');
}
return response.json();
}Tips to Stay Within Limits
- ✓Cache responses - Store activity data locally instead of fetching repeatedly
- ✓Use webhooks - Get push notifications instead of polling for changes
- ✓Batch requests wisely - Use
per_page=200to get more data per request - ✓Queue and throttle - Spread requests over time for background processing
Security
Protect Your Credentials
- ✗Never commit Client Secret to version control
- ✗Never expose tokens in client-side JavaScript
- ✗Never log tokens or include them in error messages
- ✓Use environment variables for all credentials
- ✓Store tokens encrypted in your database
OAuth Security
- ✓Use the
stateparameter to prevent CSRF attacks - ✓Validate that returned scopes match what you requested
- ✓Exchange authorization codes immediately (they expire quickly)
- ✓Always use HTTPS for your callback URL in production
// Using state parameter to prevent CSRF
const state = crypto.randomBytes(16).toString('hex');
// Store state in session
req.session.oauthState = state;
const authUrl = new URL('https://www.strava.com/oauth/authorize');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'activity:read_all');
authUrl.searchParams.set('state', state); // Include state
// In callback, verify state matches
app.get('/callback', (req, res) => {
if (req.query.state !== req.session.oauthState) {
return res.status(403).send('Invalid state parameter');
}
// ... continue with token exchange
});Error Handling
| Status Code | Meaning | Action |
|---|---|---|
| 400 | Bad Request | Check request parameters |
| 401 | Unauthorized | Token expired or invalid - refresh or re-auth |
| 403 | Forbidden | Missing required scope |
| 404 | Not Found | Resource doesn't exist or no access |
| 429 | Too Many Requests | Rate limited - wait and retry |
| 500 | Server Error | Strava issue - retry with backoff |
Robust Error Handler
class StravaAPIError extends Error {
constructor(status, message, response) {
super(message);
this.status = status;
this.response = response;
}
}
async function stravaFetch(endpoint, accessToken, options = {}) {
const response = await fetch(`https://www.strava.com/api/v3${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${accessToken}`,
...options.headers
}
});
if (!response.ok) {
const errorBody = await response.text();
switch (response.status) {
case 401:
throw new StravaAPIError(401, 'Token expired or invalid', errorBody);
case 403:
throw new StravaAPIError(403, 'Insufficient permissions', errorBody);
case 404:
throw new StravaAPIError(404, 'Resource not found', errorBody);
case 429:
const retryAfter = response.headers.get('Retry-After') || 900;
throw new StravaAPIError(429, `Rate limited. Retry after ${retryAfter}s`, errorBody);
default:
throw new StravaAPIError(response.status, 'API request failed', errorBody);
}
}
return response.json();
}
// Usage with error handling
try {
const activity = await stravaFetch(`/activities/${id}`, accessToken);
} catch (error) {
if (error instanceof StravaAPIError) {
if (error.status === 401) {
// Refresh token and retry
const newToken = await refreshTokens(userId);
const activity = await stravaFetch(`/activities/${id}`, newToken);
} else if (error.status === 429) {
// Schedule retry
await scheduleRetry(task, 15 * 60 * 1000);
}
}
}Token Management
Store refresh tokens separately
Keep refresh tokens in a secure, separate location from access tokens
Refresh proactively
Refresh tokens before they expire (e.g., when <5 minutes remaining)
Handle token rotation
Always save the new refresh token returned after each refresh
Handle deauthorization
When tokens fail, prompt user to re-authorize rather than retrying indefinitely
Production Deployment Checklist
- Update callback URL from localhost to production domain
- Ensure callback URL uses HTTPS
- Store credentials in environment variables
- Implement token refresh logic
- Set up webhook subscription for real-time updates
- Implement rate limit handling and backoff
- Add error logging and monitoring
- Cache API responses to reduce requests
- Handle user deauthorization gracefully
- Review Strava API Terms of Service
Strava Brand Guidelines
When building apps that integrate with Strava, follow their brand guidelines: