Stridee
Stridee 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.

Every endpoint receives every event type. Switch on type, handle what you use, and return 2xx for the rest — a handler that throws on an unfamiliar type turns a new event into a failed delivery.

What arrives

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…"
}

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.

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.

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 });

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.

Confirming a delivery

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

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

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.

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.