codetoad/strava
Laravel package for integrating the Strava API into your app. It simplifies OAuth authentication and access to athlete, activity, and stats data, making it easier to build fitness, training, and activity-tracking features with Strava.
composer require codetoad/stravaphp artisan vendor:publish --provider="CodeToad\Strava\StravaServiceProvider" then php artisan migrate. The migration creates the table the package uses to persist OAuth tokens (usually one row per user).https://www.strava.com/settings/api. Note the client ID/secret; the Authorization Callback Domain field on Strava takes a domain only, while the full redirect_uri goes in your .env.STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET, STRAVA_REDIRECT_URI (e.g. https://app.test/strava/callback).code for tokens via the facade. From there, call the athlete endpoint to confirm the connection works and render the athlete's name on a settings page.Look first at config/strava.php, the auto-discovered facade alias, and the CodeToad\Strava\Strava class itself — it's small, so read the method list rather than guessing signatures.
OAuth flow (controller-based):
Route::get('/strava/connect', fn (Strava $strava) => $strava->redirect());
Route::get('/strava/callback', function (Strava $strava) {
$strava->token(request('code')); // exchanges + persists tokens
return redirect()->route('dashboard');
})->middleware('auth');
Resolve the client after auth() so tokens are associated with the correct user (the shipped migration has a user_id foreign key for this). Never treat the client as a process-wide singleton if you support more than one athlete.
Common day-to-day calls:
$strava->athlete() — profile, measurement preference, location.$strava->athleteStats($athleteId) — YTD and recent totals for a dashboard header.$strava->activities($page, $perPage, $before, $after) — the workhorse; pass Unix after/before for incremental syncs.$strava->activity($id, $includeAllEfforts) — detail view with segment efforts.$strava->streams($id, ['time','latlng','heartrate','watts']) — chart/heatmap feeds.$strava->upload($file, 'gpx', ...) then poll the upload endpoint until status is ready.Sync workflow: a scheduled job per user pulls activities with after = max(local created_at), upserts into your own activities table keyed on Strava's numeric ID, then a second job hydrates streams for rows missing them. Chunk per user, respect the 15-minute window, and track a last_synced_at column.
Webhooks over polling: subscribe once at the app level to Strava's push_subscriptions endpoint, answer the one-time GET challenge, and on POST events queue a fetch for object_id. This is dramatically cheaper than a 15-minute loop and is the right default once you have more than a handful of athletes.
Testing: wrap the package in your own StravaClient service so you can swap it for a fake bound in the container. Faking the facade directly is brittle.
429, and inspect the X-RateLimit-* response headers before you retry.encrypted and write them atomically.redirect_uri must match character-for-character what you configured. The Strava dashboard's "Authorization Callback Domain" is not the same field — put the domain there, the full URI in your request.read won't expose private activities or streams. Request activity:read_all (and read for profile/stats). An empty streams payload is almost always a scope problem.after bound will re-download the entire history every run. Default before to the newest known activity ID/time.204 No Content on some mutations (deletes, update). Guard against null before calling ->json().201 means accepted, not processed. Poll the upload ID and treat error statuses (duplicate of ..., malformed) as first-class cases in your UI.hub.challenge value as JSON; a Laravel route returning the array from request('hub.challenge') is enough.auth() is empty. Pass the user (or resolved token) explicitly to the job — otherwise the client resolves to the wrong account or throws.{"message":"Authorization Error","errors":[...]}); without the response body you will spend hours guessing.composer.json and verify method signatures against vendor/codetoad/strava/README.md at install time rather than relying on blog posts.How can I help you explore Laravel packages today?