Stridee
Stridee Platform Docs

Webhooks

Register an endpoint, handle a delivery in the right order, and recover when yours was down.

Every event reaches you as a POST to your endpoint. Two checks protect it, and your handler does both:

AnswersPage
The webhook-signature headerDid Stridee send this?Verifying a delivery
The encrypted bodyCan only you read it?Encrypted deliveries

There is no shared secret to store, rotate or leak.

Registering an endpoint

Add the URL on the Webhooks screen and choose an encryption key — you can generate one from the same form. It has to be https:// and resolve to a public address. There is no cleartext mode and no verification step: the endpoint is live the moment it is saved.

Choosing events

By default an endpoint receives every event type, including types added later. To narrow it, pick Only selected on the form, or send enabled_events to POST /platform/webhooks or PATCH /platform/webhooks/{id}:

  • ["*"] is everything, and the default.
  • An exact type, such as activity.created, gets only that type.
  • A family, such as wellness.*, gets every type in it, including ones added later.

A name we don't send is refused with a 400. The list can't be empty; if an endpoint needs nothing, remove it. A change applies from the next event, and deliveries already made are unaffected.

Two types arrive whatever you subscribe to. ping is sent because you pressed the button for that endpoint. account.deleted is sent because it tells you to erase a user's data.

However you subscribe, switch on type and return 2xx for anything you don't handle. A handler that throws on an unfamiliar type turns a new event into a failed delivery.

JSON
{ "enabled_events": ["activity.created", "wellness.*"] }

What arrives

webhook-id is the delivery id — stable across resends, so it is what you dedupe on. The same id and timestamp are repeated inside the signature, and that signed copy is the one to trust. enc opens to an event.

HTTP
POST /webhooks HTTP/1.1
webhook-id: c41e9b02-7a3d-4e58-8f19-6b0d2c85af73
webhook-timestamp: 1770124811
webhook-signature: eyJhbGciOiJFZERTQSIsImtpZCI6…..iHpBfMmUqShn7ikCKRQc_GbqAGfi…
Content-Type: application/json

{
  "id": "c41e9b02-7a3d-4e58-8f19-6b0d2c85af73",
  "type": "encrypted",
  "enc": "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTI1NkdDTSIsImtpZCI6IjVhOGYzMWQ2LTBjOTQt…"
}

Handling a delivery

In this order — each step is cheaper than the next, so a forged POST costs you one signature check and nothing more.

verifySignature and assertFresh are on Verifying a delivery; decrypt is the Quickstart handler plus the kid lookup on Encrypted deliveries.

Answer fast. Nothing retries automatically, so a handler that times out while calling three other services has lost that event.

handler.js
// 1. Who sent it? Verify the raw bytes, before parsing anything.
const { protectedHeader } = await verifySignature(req.headers['webhook-signature'], rawBody);

// 2. Is it recent?
assertFresh(protectedHeader['webhook-timestamp']);

// 3. Open it with the key named by `kid`.
const { enc } = JSON.parse(rawBody);
const event = await decrypt(enc);

// 4. Have we seen it? Still echo the nonce if so.
if (await seen(protectedHeader['webhook-id'])) return res.json({ nonce: event.nonce });

// 5. Store it, answer, and do the real work afterwards.
await store(event);
res.json({ nonce: event.nonce });

Confirming a delivery

Every delivery carries a fresh random nonce inside the ciphertext, and your 2xx must return it at the top level:

Only something holding your private key can produce it, so the echo proves your service opened the body — not just that some server answered. A bare 200, {"ok":true} or a nested nonce count as no echo.

A missing echo does not fail the delivery: you returned 2xx, so the event is yours. It is recorded and shown in the console, which is how a wrong key, an undeployed private half or a proxy answering for you shows up as delivered but not opened instead of healthy.

What your handler answers with
{ "nonce": "Kd4nWpLbEa9xTvRm2Cj7Lz0Bq2vNhCz7" }

Sending a ping

A ping is a real delivery — minted, signed, sealed and recorded like any other — so it tests the whole path:

  • Locally: stridee trigger ping sends one to every running stridee listen.
  • Production: Send ping on the Webhooks screen. It reports the status code and timing, or which hop failed, and whether the nonce came back. Capped at 10 per endpoint per minute.

A ping has no user_id or provider — no athlete was involved.

When a delivery fails

Nothing is retried automatically. A delivery is POSTed once. If your endpoint times out, refuses the connection or answers 5xx, it stops there. Every attempt is recorded on the endpoint's row with the reason — dns, tls, timeout, connection, blocked or status — and the round-trip time.

To recover:

  • Resend any delivery from the console, or with POST /platform/webhooks/{id}/deliveries/{delivery_id}/resend. The webhook-id stays the same, so your dedupe makes it safe; the nonce is new, so echo the one you just decrypted; and it is sealed to the endpoint's key as it is now, which is what fixes a rotation you got wrong.
  • Backfill activities you missed during an outage from GET /v1/activities.

Something wrong or missing on this page? Tell us in Discord. Need something the API doesn’t do yet? Request it on the roadmap.