# Strava Webhooks

## When to Use This Skill

* How do I receive Strava webhooks?
* How do I set up a Strava push subscription?
* How do I implement the Strava subscription validation (GET `hub.challenge`) handshake?
* Why is my Strava subscription creation failing / callback validation failing?
* How do I handle Strava `activity` and `athlete` events?
* How do I detect a Strava athlete deauthorization?

## How Strava Webhooks Differ

Strava push events are NOT cryptographically signed — there is no per-event
signature, HMAC, or shared-secret header to verify on each POST. Authenticity is
established once, at subscription time, via a GET handshake:

1. You POST to `https://www.strava.com/api/v3/push_subscriptions` with
  `client_id`, `client_secret`, `callback_url`, and a self-chosen `verify_token`.
2. Strava immediately GETs your `callback_url` with `hub.mode=subscribe`,
  `hub.challenge=<random>`, and `hub.verify_token=<your token>`.
3. You confirm `hub.verify_token` matches your token and respond within 2
  seconds with HTTP `200` and JSON body `{"hub.challenge":"<echoed value>"}`.

After that, Strava POSTs thin event payloads (an `object_id`, not full data) to
the same callback. Acknowledge every event with `200` within 2 seconds or
Strava retries (up to 3 total attempts). Fetch full activity/athlete data from
the [Strava REST API](https://developers.strava.com/docs/reference/) using the
`object_id`. Only ONE subscription is allowed per API application.

## Verification (core)

There is no signature to check on events — the security boundary is the GET
validation handshake. Compare `hub.verify_token` against your stored token with a
timing-safe comparison, then echo `hub.challenge`:

```javascript
const crypto = require('crypto');

// GET /webhooks/strava — subscription validation handshake
function handleValidation(query, expectedToken) {
  const mode = query['hub.mode'];
  const token = query['hub.verify_token'] || '';
  const challenge = query['hub.challenge'];

  const a = Buffer.from(token);
  const b = Buffer.from(expectedToken);
  const tokenOk = a.length === b.length && crypto.timingSafeEqual(a, b);

  if (mode === 'subscribe' && tokenOk) {
    return { status: 200, body: { 'hub.challenge': challenge } }; // exact key name
  }
  return { status: 403, body: 'Forbidden' };
}

```

> For complete handlers (GET validation + POST event dispatch) with tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/strava-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/strava-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/strava-webhooks/examples/fastapi/)

## Common Event Types

Events are identified by `object_type` + `aspect_type` (there is no single event
name string). All values below are exact.

| `object_type` | `aspect_type` | Triggered When |
| --- | --- | --- |
| `activity` | `create` | An athlete uploads/creates a new activity |
| `activity` | `update` | An activity's title, type, or privacy changes |
| `activity` | `delete` | An activity is deleted |
| `athlete` | `update` | Athlete deauthorizes your app (`updates` = `{"authorized":"false"}`) |

`updates` for an `activity` `update` may contain `title`, `type`, and `private`
(`"true"` / `"false"`). A single save can produce multiple events.

> For the full reference, see [Strava Webhook Events API](https://developers.strava.com/docs/webhooks/).

## Event Payload Structure

```json
{
  "object_type": "activity",
  "object_id": 1360128428,
  "aspect_type": "create",
  "owner_id": 134815,
  "subscription_id": 120475,
  "event_time": 1516126040,
  "updates": {}
}

```

## Environment Variables

```bash
STRAVA_CLIENT_ID=12345                 # Strava API application ID
STRAVA_CLIENT_SECRET=xxxxxxxx          # Strava API application secret
STRAVA_VERIFY_TOKEN=your_random_token  # Self-chosen token echoed during validation
STRAVA_SUBSCRIPTION_ID=120475          # Optional: reject events from other subscriptions

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 strava --path /webhooks/strava

```

Then create the subscription so Strava validates your callback:

```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-tunnel-url>/webhooks/strava \
  -F verify_token=$STRAVA_VERIFY_TOKEN

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/strava-webhooks/references/overview.md) - Strava webhook concepts, events, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/strava-webhooks/references/setup.md) - Create/view/delete a push subscription
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/strava-webhooks/references/verification.md) - Validation handshake details and gotchas