Skip to content

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. 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:

Submit a rate limit increase request

Reading Rate Limit Headers

Every API response includes headers showing your current usage:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed (15min, daily)
X-RateLimit-UsageCurrent usage (15min, daily)
X-ReadRateLimit-LimitMaximum read (non-upload) requests allowed (15min, daily)
X-ReadRateLimit-UsageCurrent 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 CodeMeaningAction
400Bad RequestCheck request parameters
401UnauthorizedToken expired or invalid - refresh or re-auth
403ForbiddenAccess refused (missing scopes typically surface as 401 instead)
404Not FoundResource doesn't exist or no access
429Too Many RequestsRate limited - wait and retry
500Server ErrorStrava 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

1

Store refresh tokens separately

Keep refresh tokens in a secure, separate location from access tokens

2

Refresh proactively

Refresh tokens before they expire (e.g., when <5 minutes remaining)

3

Handle token rotation

Always save the new refresh token returned after each refresh

4

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 and into a separate Strava 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

Hear when Strava's API changes

When Strava changes its API, its terms or its review process, we email a short, sourced summary of what it means for apps, plus a monthly note on what changed. Reply "unsubscribe" any time.