Why Is My Strava Webhook Not Firing?
Updated July 9, 2026
Your Strava webhook isn't firing, and the most likely reason is the one that's easiest to miss: the subscription was never created in the first place. Creating a subscription is a two-step handshake, and if the validation step failed, Strava silently gave up and no events will ever arrive. The fastest fix is to confirm a subscription actually exists, and if it doesn't, get your callback endpoint answering Strava's validation GET correctly.
Below are the causes ranked from most to least common, each with the concrete check and fix. If your problem is really that data shows up late rather than never, see Wearable data delayed or missing. For the full happy-path setup, see the Strava API integration guide.
Most likely causes, ranked
- The subscription was never created because the validation
GEThandshake failed — your callback didn't echohub.challengeas JSON with HTTP 200 within ~2 seconds. - Your callback isn't a public HTTPS URL.
localhost, private IPs, and self-signed certs won't validate. - A stale subscription owns the only slot. Strava allows exactly one subscription per application, so a second create silently fails.
- Events are firing — but they're lightweight pointers, and your handler crashes trying to read activity fields that aren't in the payload.
- Private activities are invisible because the athlete granted
activity:readinstead ofactivity:read_all.
Step 1: Confirm a subscription actually exists
Before debugging anything else, ask Strava whether your app even has a subscription. View the current one for your application:
curl -G https://www.strava.com/api/v3/push_subscriptions \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
An empty array ([]) means no subscription exists, so nothing will ever fire — go to Step 2. If a subscription is returned, note its id and callback_url; if that URL is stale or wrong, jump to Step 4 to delete and recreate it.
Step 2: Answer the validation handshake correctly
Creating a subscription is a two-step handshake. You POST to request it, and Strava then immediately issues a GET to your callback_url to validate it. If that GET isn't answered correctly, the subscription is never created.
Step 2a — request the subscription:
curl -X POST https://www.strava.com/api/v3/push_subscriptions \
-F client_id=YOUR_CLIENT_ID \
-F client_secret=YOUR_CLIENT_SECRET \
-F callback_url=https://example.com/webhook \
-F verify_token=STRAVA
Step 2b — Strava calls your callback with a validation GET, for example:
GET https://example.com/webhook?hub.verify_token=STRAVA&hub.challenge=15f7d1a91c1f40f8a748fd134752feb3&hub.mode=subscribe
Your endpoint must, within two seconds, return HTTP 200 and echo hub.challenge back as application/json:
{ "hub.challenge": "15f7d1a91c1f40f8a748fd134752feb3" }
Per Strava's docs, the most common reason a subscription fails to be created is a failure to respond to this validation GET in a timely manner, or failing to echo the hub.challenge field correctly. A minimal handler looks like this:
// GET /webhook — Strava's subscription validation
app.get("/webhook", (req, res) => {
const mode = req.query["hub.mode"];
const token = req.query["hub.verify_token"];
const challenge = req.query["hub.challenge"];
// Optional but recommended: verify the token you sent on create
if (mode === "subscribe" && token === "STRAVA") {
// Echo the challenge back as JSON, status 200, fast — no heavy work here
return res.status(200).json({ "hub.challenge": challenge });
}
return res.sendStatus(403);
});
Handshake checklist:
- Endpoint returns 200 — not a
301/302redirect, and not401behind auth middleware. - Body is exactly
{"hub.challenge": "<value>"}withContent-Type: application/json. - The
hub.verify_tokenmatches theverify_tokenyou sent on create. - The handler is fast (under 2 seconds) — do no database or network work in the validation path.
Step 3: Make sure the callback is public HTTPS
The callback URL must be publicly reachable over HTTPS. localhost, private/internal IPs, and untrusted or self-signed certificates will not validate, so the handshake in Step 2 fails before your code ever runs. During development, expose a public HTTPS URL with a tunnel such as ngrok — this is exactly what Strava's own webhook example uses. Also confirm no WAF or auth middleware is silently blocking Strava's request.
Step 4: Free the single subscription slot
Each application may have only one subscription, and that single subscription receives events for all athletes who authorized your app. A very common "not firing" story is: you spun up a new callback URL, tried to create a fresh subscription, and it failed because the old one still owns the slot. Delete the stale subscription (using its id from Step 1) to free the slot, then recreate with Step 2:
curl -X DELETE \
"https://www.strava.com/api/v3/push_subscriptions/SUBSCRIPTION_ID?client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET"
Step 5: Handle the event as a pointer, not the activity
If the subscription exists and validates but you still see "nothing," the events may be arriving and your handler may be quietly crashing. Webhook payloads are small notifications, not the activity data. A POST to your callback looks like this:
{
"aspect_type": "create",
"event_time": 1549560669,
"object_id": 1360128428,
"object_type": "activity",
"owner_id": 134815,
"subscription_id": 120475,
"updates": {}
}
Return 200 to Strava immediately, then process asynchronously. To get the actual activity, call the REST API with that athlete's access token, for example GET /activities/{object_id} using object_id from the event. A handler that tries to read activity fields (distance, name, etc.) straight off the pointer, or whose follow-up fetch fails on an expired token, looks exactly like "the webhook is broken." Add logging as close to the network edge as possible — an unhandled exception in payload processing is a frequent cause of "missing updates."
Step 6: Grant activity:read_all for private activities
If a webhook fires but the follow-up fetch returns nothing (or 404s) specifically for private or hidden activities, the athlete most likely authorized only activity:read. Private activities require the activity:read_all scope, granted at OAuth time. Request it in your authorization URL and have affected athletes re-consent. (Verify current scope naming on the Strava authentication docs.)
Still stuck? Diagnostic checklist
Run these in order:
- Does a subscription exist?
GET /push_subscriptions(Step 1). Empty array = nothing will ever fire. - Is the callback public HTTPS? No
localhost, no self-signed cert, no auth/WAF in front of it. - Test reachability yourself. Hit your callback with a
GETcarrying a fakehub.challenge— it should echo it. ThenPOSTa sample event body — it should return 200. Strava's own advice is toPOSTto your callback manually and confirm a 200. - Only one slot. If create fails, delete the stale subscription first (Step 4).
- Log at the edge. Confirm events aren't arriving and silently erroring in your handler.
- Check the scope. Missing private activities usually means
activity:readinstead ofactivity:read_all.
If events are arriving but appear late rather than never, that's a different problem — see Wearable data delayed or missing. For end-to-end setup including OAuth and scopes, see the Strava API integration guide.
Frequently asked questions
- How do I check whether my Strava webhook subscription actually exists?
- Send a GET request to https://www.strava.com/api/v3/push_subscriptions with your client_id and client_secret. If it returns an empty array, no subscription exists and no events will ever fire, so you need to create one and pass the validation handshake.
- Why does creating my Strava subscription fail even though my server is running?
- When you POST to create a subscription, Strava immediately issues a validation GET to your callback_url. If your endpoint does not return HTTP 200 and echo hub.challenge back as application/json within about two seconds, the subscription is never created. Localhost, self-signed certs, redirects, and auth middleware in front of the callback all cause this to fail.
- Can I have more than one Strava webhook subscription per app?
- No. Each application may have only one subscription, and it receives events for all athletes who authorized the app. If you try to create a second one while a stale subscription still owns the slot, the create fails. Delete the old subscription first with DELETE push_subscriptions.
- The webhook fires but my code errors. Is the webhook broken?
- Usually not. Strava events are lightweight pointers containing fields like object_id and owner_id, not the full activity. Return 200 immediately, then fetch the activity from the REST API using object_id and the athlete's token. A handler that crashes reading missing fields, or whose follow-up fetch fails on an expired token, looks like a webhook that isn't firing.
- Why can't I see private activities from the webhook?
- Private and hidden activities require the activity:read_all scope, granted at OAuth time. If the athlete authorized only activity:read, your follow-up fetch returns nothing for those activities. Request activity:read_all and have the athlete re-consent. Verify current scope naming on the Strava authentication docs.
Keep reading
Independent comparison, last reviewed July 9, 2026. Pricing, rate limits, and feature availability change often — confirm current details in each provider’s official documentation before you commit. Product and company names are trademarks of their respective owners; AIFitnessAPI is not affiliated with, endorsed by, or sponsored by any product listed here.
← All troubleshooting · by AIFitnessAPI