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 — 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

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, '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