You POST to https://www.strava.com/api/v3/push_subscriptions and get back:
{"message":"Bad Request","errors":[
{"resource":"PushSubscription","field":"callback url","code":"not verifiable"}]}
Strava's docs say the most common cause of subscription-creation failure is
“a failure to respond in a timely manner to the validation GET request, or failure to
correctly echo the hub.challenge field.” That's the whole game: when you
POST the creation request, Strava immediately sends a GET to your
callback_url and grades your answer. Here's what it sends, what it expects,
and the five ways handlers get it wrong.
GET https://your-callback?hub.verify_token=STRAVA&hub.challenge=15f7d1a91c1f40f8a748fd134752feb3&hub.mode=subscribe
Your endpoint must respond within two seconds with status
200 and echo the challenge as JSON, content type
application/json:
{ "hub.challenge": "15f7d1a91c1f40f8a748fd134752feb3" }
The JSON key is the literal string "hub.challenge" — dot included.
Pass, and your original POST returns {"id": 1} with your subscription id.
Fail, and you get the 400 above.
Strava's handshake looks exactly like
Meta's hub.challenge check —
same hub.mode/hub.challenge/hub.verify_token query
params — so people port their WhatsApp/Messenger handler. But Meta wants the
raw challenge string as the response body; Strava wants a JSON object with a
"hub.challenge" key. A plain-text echo that passes Meta fails Strava, and
vice versa. Related trap: echoing hub.verify_token instead of
hub.challenge — the token is there for you to check the request
really came from Strava's subscription service (it's the verify_token you
chose in the creation POST); it is not what you send back.
Events arrive as POSTs, so many handlers register a POST-only route. The validation is a GET to the same URL — a 404/405 on GET means “not verifiable” every time, even though your event handling is perfect.
Cold-starting serverless functions, spinning up a tunnel, or doing any real work before responding can eat the entire two-second budget. Answer the GET first; do everything else after.
The subscription-creation parameters must be sent as HTTP form data
(-F/urlencoded — not a JSON body): client_id,
client_secret, callback_url (max 255 characters), and your
chosen verify_token. And note each application may only have one
subscription — to change the callback URL, view the existing one
(GET /push_subscriptions?client_id=…&client_secret=…), DELETE
it, then re-create.
localhost, private IPs, or a broken TLS chain — Strava's subscription
service has to reach the URL from the internet. For local development, see
webhooks to localhost.
A Hookden bin can pass Strava's validation before you've written any handler, so you can watch real event payloads first:
200, content type application/json, response body:
{"hub.challenge":"{{query.hub.challenge}}"}curl -X POST https://www.strava.com/api/v3/push_subscriptions \ -F client_id=YOUR_ID \ -F client_secret=YOUR_SECRET \ -F callback_url=https://hookden.pages.dev/h/YOUR-BIN \ -F verify_token=PICK-ANYTHING
hub.verify_token echo).
Ride, rename an activity, and the event POSTs appear live.Want to see what the event POSTs look like before you subscribe? The Strava activity update payload example shows a realistic sample body — and one click loads it into a live bin.
This is the fourth handshake style we've catalogued: Slack echoes a POST body field, Zoom wants an HMAC of the token, Meta wants a raw GET echo — and Strava wants the GET echo wrapped in JSON.
Strava event deliveries carry no signature header at all — the
verify_token only appears in the validation GET, never on events. Treat the
payload as a pointer, not a fact: it contains object_type
(activity/athlete), object_id,
aspect_type (create/update/delete),
an updates hash, owner_id, subscription_id and
event_time — then you fetch the real object from the API with your access
token, which is your integrity check. Notable semantics, straight from the docs:
updates keys for activity updates are title, type,
private; an athlete deauthorizing your app arrives as an athlete event with
"updates": {"authorized": "false"}; and with only activity:read
scope, an athlete flipping an activity to “Only You” reaches you as a
delete event (and back as a create) — respect it.
One “save” by the athlete can also fan out into multiple events, because some
activity attributes update asynchronously.
Each event POST must be acknowledged with a 200 within two seconds; failed pushes are retried up to a total of three attempts, then the event is gone — do your processing async. Full comparison in the retry-schedule table across 26 providers.
Related: Meta's hub.challenge check (the plain-text sibling), Twitch EventSub's challenge, and webhooks to localhost for developing the handler.
No signup needed. Or from your terminal: curl https://hookden.pages.dev/new
← All guides · Signature debugger · Payload examples · Docs · Hookden vs webhook.site