---
title: "Strava API Best Practices | Apps for Strava"
canonical: https://appsforstrava.com/developers/best-practices
source: Markdown mirror of https://appsforstrava.com/developers/best-practices — the HTML page is canonical.
---

# 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. Self-upgrading in the API Settings Dashboard raises the overall limit to 400 per 15 minutes and 4,000 per day (read: 200 / 2,000).

### Requesting Higher Rate Limits

Start in the [API Settings Dashboard](https://www.strava.com/settings/api). As of 2026, Standard Tier apps can self-upgrade there with no review, up to 10 connected athletes and higher rate limits. Past 10 athletes, you submit your app for review; the Standard Tier covers up to 9,999 athletes after approval, and the Extended Access Tier is for apps serving 10,000 or more. Before requesting more:

-   1.Verify your app is actually hitting limits (check your [API settings](https://www.strava.com/settings/api))
-   2.Optimize your API usage first. Use webhooks instead of polling, and throttle backfills
-   3.Review the [API Agreement](https://www.strava.com/legal/api), [API Policy](https://www.strava.com/legal/api_policy), and [Brand Guidelines](https://developers.strava.com/guidelines)

[Submit a rate limit increase request](https://share.hsforms.com/1VXSwPUYqSH6IxK0y51FjHwcnkd8)

### 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) |
| X-ReadRateLimit-Limit | Maximum read (non-upload) requests allowed (15min, daily) |
| X-ReadRateLimit-Usage | Current read (non-upload) 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 (read limits are usually the ones you hit first)
  const limitHeader = response.headers.get('X-RateLimit-Limit');
  const usageHeader = response.headers.get('X-RateLimit-Usage');
  const readLimitHeader = response.headers.get('X-ReadRateLimit-Limit');
  const readUsageHeader = response.headers.get('X-ReadRateLimit-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}`);
  }

  if (readLimitHeader && readUsageHeader) {
    const [readLimit15min] = readLimitHeader.split(',').map(Number);
    const [readUsage15min] = readUsageHeader.split(',').map(Number);

    // Warn if approaching the read limit
    if (readUsage15min > readLimit15min * 0.8) {
      console.warn('Approaching 15-minute read rate limit');
    }
  }

  // Handle 429 Too Many Requests
  if (response.status === 429) {
    // Strava doesn't document a Retry-After header; wait for the next 15-minute window
    console.error('Rate limited. Retry after the 15-minute window resets');
    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=200` to 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 `state` parameter to prevent CSRF attacks
-   ✓Validate that returned scopes match what you requested
-   ✓Exchange authorization codes immediately, and treat them as short-lived and single-use
-   ✓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 | Access refused (missing scopes typically surface as 401 instead) |
| 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 |

### 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, 'Forbidden', errorBody);
      case 404:
        throw new StravaAPIError(404, 'Resource not found', errorBody);
      case 429:
        // No documented Retry-After header; back off until the 15-minute window resets
        throw new StravaAPIError(429, 'Rate limited', 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 the Strava API Agreement and API Policy

## API Policy Rules to Know

On June 1, 2026 Strava moved its use restrictions out of the [API Agreement](https://www.strava.com/legal/api) and into a separate [Strava API Policy](https://www.strava.com/legal/api_policy) that the agreement incorporates by reference. Read both. The rules that catch developers most often:

✗**No AI use of Strava data** (§5.3). Not just training: the policy bars using Strava data "in connection with the development, training, evaluation, or operation of any AI Application," and lists retrieval-augmented generation and "ingestion into a context window or working memory" by name. Strava's own MCP Connector is the only carve-out.

✗**No MCP servers, proxies, or aggregators** (§5.16). You may not operate an MCP server, pass-through proxy, no-code/AI platform, or any intermediary that re-exposes Strava data, and you may not share API tokens across services or users.

✗**No persistent indexes** (§5.5). Vector stores, embedding stores, search indexes, knowledge graphs, and archives built from Strava data are prohibited. A transient cache of up to seven days (§6.2) is allowed.

✗**Only show a user their own data.** Strava data from one athlete may only be displayed or disclosed to that athlete, even if it is publicly visible on Strava.

✓**Know your tier** (§3.3). Standard Tier covers apps up to 10 and up to 9,999 athletes and requires an active Strava subscription; the Extended Access Tier (10,000+ athletes) is approved case by case and is not subject to the subscription requirement.

## Strava Brand Guidelines

When building apps that integrate with Strava, follow their brand guidelines:

✓Use "Compatible with Strava" or "Powered by Strava" messaging, and "View on Strava" for links back to Strava data

✓Display "Powered by Strava" logo when showing Strava data

✗Don't imply endorsement or partnership with Strava

✗Don't modify the Strava logo or use it as your app icon

[View Full Brand Guidelines](https://developers.strava.com/guidelines/)

## Ready to Build?

- [Getting Started](https://appsforstrava.com/developers/getting-started): Create your first Strava app
- [Code Examples](https://appsforstrava.com/developers/examples): Ready-to-use code snippets
