Best Practices

Guidelines for building reliable, secure, and performant Strava integrations.

Rate Limiting

Default Rate Limits

  • 200requests per 15 minutes
  • 2,000requests per day

Requesting Higher Rate Limits

If your app is approaching capacity, you can request a rate limit increase through Strava's Developer Program. Before applying:

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)

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=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 (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 CodeMeaningAction
400Bad RequestCheck request parameters
401UnauthorizedToken expired or invalid - refresh or re-auth
403ForbiddenMissing required scope
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, '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

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 Strava API Terms of Service

Strava Brand Guidelines

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

Use "Compatible with Strava" or "Works with Strava" messaging
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