Skip to content
AFAIFitnessAPI
Troubleshooting

Why Is My Strava Webhook Not Firing?

Updated July 9, 2026

The most common reason a Strava webhook never fires is that the subscription was never created: creating one is a two-step handshake, and if your callback fails to echo the hub.challenge as JSON with HTTP 200 within about two seconds, Strava silently abandons it. First confirm a subscription actually exists by calling GET push_subscriptions with your client_id and client_secret; an empty array means nothing will ever fire. Then make sure your callback is a public HTTPS URL that answers the validation GET correctly, and remember only one subscription is allowed per application.

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

  1. The subscription was never created because the validation GET handshake failed — your callback didn't echo hub.challenge as JSON with HTTP 200 within ~2 seconds.
  2. Your callback isn't a public HTTPS URL. localhost, private IPs, and self-signed certs won't validate.
  3. A stale subscription owns the only slot. Strava allows exactly one subscription per application, so a second create silently fails.
  4. Events are firing — but they're lightweight pointers, and your handler crashes trying to read activity fields that aren't in the payload.
  5. Private activities are invisible because the athlete granted activity:read instead of activity: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/302 redirect, and not 401 behind auth middleware.
  • Body is exactly {"hub.challenge": "<value>"} with Content-Type: application/json.
  • The hub.verify_token matches the verify_token you 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 GET carrying a fake hub.challenge — it should echo it. Then POST a sample event body — it should return 200. Strava's own advice is to POST to 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:read instead of activity: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