# Guide to Strava Webhooks: Features and Best Practices

Strava webhooks notify your application when athletes' activities and profiles change: an activity is created, an activity is updated or deleted, an athlete deauthorizes your app. If you're building on Strava, webhooks are how you react without polling every athlete's data on a timer.

This guide covers how Strava webhooks work, the events you'll handle, the subscription validation handshake (there's no per-event signature, so this matters), the thin-payload model, and the best practices for production.

## What are Strava webhooks?

Strava uses a push subscription model. You create one subscription per API application, and Strava then POSTs events for every athlete who has authorized your app. Each event is thin: it carries an `object_id` and a few fields, and you fetch the full data from the REST API.

The important thing to understand up front: Strava webhook events are not cryptographically signed. There's no per-event HMAC or signature to verify. Authenticity is established once, at subscription time, through a validation handshake plus a `verify_token` you choose.

## Strava webhook features

| Feature | Details |
| --- | --- |
| Configuration | `POST /api/v3/push_subscriptions` (one subscription per application) |
| Validation | GET handshake: echo `hub.challenge` as JSON within 2 seconds |
| Per-event signature | None (no HMAC / shared-secret signature) |
| Shared token | `verify_token` you choose, checked at subscription time |
| Payload | Thin: `object_type`, `aspect_type`, `object_id`, `owner_id`, `subscription_id`, `event_time`, `updates` |
| Data fetch | Call the REST API with the athlete's token |
| Acknowledgement | 200 within 2 seconds |
| SDK | No official SDK (community libraries only) |

## Common events

Strava events combine an `object_type` and an `aspect_type`:

| Event | Fires when |
| --- | --- |
| `activity` / `create` | An athlete uploads or creates an activity |
| `activity` / `update` | An activity's title, type, or privacy changes (see `updates`) |
| `activity` / `delete` | An activity is deleted |
| `athlete` / `update` | An athlete updates their profile, or deauthorizes your app (`updates.authorized: "false"`) |

The payload tells you what changed by ID; fetch the full activity or athlete from the REST API when you need the details.

## Setting up Strava webhooks

Create a subscription by POSTing to `https://www.strava.com/api/v3/push_subscriptions` with your `client_id`, `client_secret`, `callback_url`, and a `verify_token` you choose:

```bash
curl -X POST "https://www.strava.com/api/v3/push_subscriptions" \
  -F client_id=$STRAVA_CLIENT_ID \
  -F client_secret=$STRAVA_CLIENT_SECRET \
  -F callback_url=https://your-app.example.com/webhook \
  -F verify_token=$YOUR_VERIFY_TOKEN

```

Strava immediately GETs your `callback_url` to validate it (the handshake below). Only one subscription is allowed per application.

## Securing Strava webhooks

There's no per-event signature, so the trust mechanism has two parts.

### The validation handshake

When you create the subscription, Strava sends a GET to your `callback_url` with `hub.mode=subscribe`, a random `hub.challenge`, and your `hub.verify_token`. Confirm the token matches, then respond within 2 seconds with a 200 and a JSON body echoing the challenge:

```javascript
const VERIFY_TOKEN = process.env.STRAVA_VERIFY_TOKEN;

app.get("/webhook", (req, res) => {
  if (req.query["hub.mode"] === "subscribe" &&
      req.query["hub.verify_token"] === VERIFY_TOKEN) {
    return res.status(200).json({ "hub.challenge": req.query["hub.challenge"] });
  }
  res.sendStatus(403);
});

```

### Event POSTs

Because event POSTs aren't signed, you can't cryptographically prove a given POST came from Strava. Reduce the risk that comes with an unauthenticated endpoint:

```javascript
app.post("/webhook", express.json(), (req, res) => {
  res.sendStatus(200); // ack within 2 seconds

  const { object_type, aspect_type, object_id, owner_id } = req.body;
  // Treat the POST as an untrusted trigger: fetch the real data from the
  // Strava REST API using owner_id's stored token before acting on it.
  enqueue(() => syncFromStrava(object_type, object_id, owner_id));
});

```

* Keep the callback URL secret and hard to guess (a long random path segment).
* Treat every POST as an untrusted trigger and fetch the authoritative data from the REST API with the athlete's token, so a spoofed POST can't feed you fake data.
* Add your own endpoint auth (a secret path or a header your infrastructure checks) since Strava adds none.

## Strava webhook limitations and pain points

### No per-event signature

The Problem: Strava doesn't sign event POSTs. Any request to your callback URL looks like a legitimate event, so you can't verify authenticity cryptographically.

Why It Happens: Strava's design authenticates the subscription once (handshake + `verify_token`) rather than signing each event.

Workarounds:

* Treat POSTs as triggers and fetch real data from the REST API.
* Keep the callback URL secret and add your own endpoint auth.

How Hookdeck Can Help: Hookdeck gives you a dedicated ingestion URL and can enforce its own authentication and filtering in front of your endpoint, adding a verification layer Strava itself doesn't provide.

### One subscription per application

The Problem: You get exactly one subscription per app. You can't split events across multiple endpoints (for example, staging and production) at the Strava level.

Why It Happens: Strava's model is one subscription per application, fanning out all authorized athletes' events to it.

Workarounds:

* Route to environments downstream of a single endpoint.
* Manage subscription creation/deletion carefully during deploys.

How Hookdeck Can Help: Hookdeck receives the single subscription and fans events out to multiple destinations (staging, production, analytics) with routing rules, so one Strava subscription can feed many consumers.

### Thin payloads and the 2-second window

The Problem: Payloads carry only IDs, so every event needs a REST API fetch, and you must ack within 2 seconds. Doing the fetch synchronously blows the window.

Why It Happens: Strava keeps payloads minimal and expects a fast ack.

Workarounds:

* Return 200 immediately and fetch data asynchronously.
* Respect the REST API rate limits when fetching.

How Hookdeck Can Help: Hookdeck acknowledges Strava within the window and durably queues events so your worker fetches from the API at a controlled rate.

### Duplicates and deauthorization

The Problem: Events can repeat, and an `athlete/update` with `updates.authorized: "false"` signals a deauthorization you must handle (stop using that athlete's token). Missing it leaves you calling the API with a revoked token.

Why It Happens: At-least-once delivery plus deauthorization delivered as a regular event.

Workarounds:

* Dedupe on `object_id` + `event_time`, and handle the deauthorization event explicitly.

How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge, so repeats don't double-process. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Best practices

### Nail the handshake

Echo `hub.challenge` as JSON with a 200 within 2 seconds, and check `hub.verify_token` matches the token you chose.

### Treat POSTs as untrusted triggers

Since events aren't signed, fetch authoritative data from the REST API and keep the callback URL secret with your own endpoint auth.

### Acknowledge fast, fetch asynchronously

Return 200 immediately and fetch full data off a queue, within the REST rate limits. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Handle deauthorization

Watch for `athlete/update` with `updates.authorized: "false"` and stop using that athlete's token.

## Conclusion

Strava webhooks use a one-subscription-per-app push model with a validation handshake and a `verify_token`, but no per-event signature, so you treat each POST as an untrusted trigger and fetch authoritative data from the REST API. Add thin payloads, a 2-second ack window, one subscription per app, and deauthorization delivered as an event, and there's real handling required.

[Hookdeck](https://hookdeck.com) adds the verification and filtering Strava doesn't provide, fans a single subscription out to multiple destinations, deduplicates, and durably queues every event at the edge, so your app processes trustworthy, unique events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Strava webhooks reliably in minutes.