# Speed Webhooks

[Speed](https://www.tryspeed.com) is a Bitcoin and Lightning payments platform (USDT on Ethereum and Tron too). Its webhooks report payment, checkout session and link, payment link, invoice, withdrawal and Connect events. Every delivery is signed with **Standard Webhooks** (`webhook-id`, `webhook-timestamp`, `webhook-signature`), using a secret that starts with **`wsec_`**.

## When to Use This Skill

* How do I receive Speed webhooks?
* How do I verify a Speed webhook signature?
* Why does Speed's `wsec_` secret not work with a Standard Webhooks library?
* How do I handle `payment.paid`, `payment.confirmed` or `checkout_session.paid` events?
* Why do Hookdeck retries of a Speed webhook fail verification after a few minutes?

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | Message ID. A retry gets a new one |
| `webhook-timestamp` | Unix seconds of this attempt |
| `webhook-signature` | One or more space-separated `v1,<base64 HMAC-SHA256>` entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. The key is the base64-**decoded** part of the secret after the prefix. Speed shows the secret as `wsec_<base64>`, but the `standardwebhooks` library (npm, PyPI) only strips `whsec_`, so swap the prefix first:

```javascript
const { Webhook } = require('standardwebhooks');

// SPEED_WEBHOOK_SECRET is the wsec_ value exactly as Speed shows it
const secret = process.env.SPEED_WEBHOOK_SECRET.replace(/^wsec_/, 'whsec_');
// rawBody: exact request body string, NOT re-serialised JSON
const prefix = req.get('webhook-id') ? 'webhook' : 'svix'; // fall back to svix-* like Hookdeck
const event = new Webhook(secret).verify(rawBody, {
  'webhook-id': req.get(`${prefix}-id`),
  'webhook-timestamp': req.get(`${prefix}-timestamp`),
  'webhook-signature': req.get(`${prefix}-signature`),
}); // returns the parsed event; throws → respond 400

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(await request.body(), {...same three keys...})` with `secret = "whsec_" + raw[len("wsec_"):]`. Speed's own docs show only a Java sample with the same construction and call verification optional; keep it on anyway.

**Hookdeck source secret.** If you receive Speed webhooks through a Hookdeck Speed source, the configuration alert reads: "Speed shows the secret with a wsec_ prefix. Replace wsec_ with whsec_ when you paste it here, or verification fails."

**Timestamp window.** The library always enforces a fixed 5-minute window. **Hookdeck retries and replays resend the original timestamp, so the library rejects late retries after 5 minutes.** A manual verifier (HMAC-SHA256 over `{id}.{timestamp}.{raw body}`, every `v1,` entry compared in constant time after a length check, timestamp required to be a plain positive integer) where the window can be dropped or widened is in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speed-webhooks/references/verification.md).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/speed-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/speed-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/speed-webhooks/examples/fastapi/).

## Event Envelope

```jsonc
{ "id": "evt_xxxxxxxxxx", "object": "events", "api_version": "2022-10-15",
  "event_type": "payment.paid",
  "data": { "object": { "id": "pi_xxxxxxxxxx", "object": "payment", "amount": 5000,
                        "currency": "SATS", "status": "paid" } },
  "livemode": false, "request": { "id": "xxxxxxxxxx" },
  "created": 1696847071760, "modified": 1696847071760 }

```

The event type is the top-level `event_type`; `object` is always `events`. `data.object` is the resource (payment, checkout session, ...). Events for connected accounts add `data.account_id`. Dedupe on the event `id` (`evt_…`): a retry keeps it but gets a new `webhook-id`.

## Common Event Types

| Event | Fires when |
| --- | --- |
| `payment.created` / `payment.paid` / `payment.confirmed` / `payment.expired` / `payment.cancelled` | A payment is created, paid, confirmed, expires or is cancelled |
| `checkout_session.created` / `.paid` / `.payment_paid` / `.deactivated` | A checkout session changes |
| `checkout_link.created` / `.paid` / `.deactivated` | A checkout link changes |
| `payment_link.created` / `.paymentreceived` / `.deactivated` | A payment link changes |
| `payment_address.paymentreceived` / `payrequest.payment_paid` / `invoice.paid` | A payment address, payrequest or invoice is paid |
| `withdraw.created` / `withdraw.paid` / `withdraw.failed` | A withdrawal progresses |
| `withdrawal_link.*` / `withdraw_request.*` | `created`, `paid`, `deactivated` on withdrawal links and requests |
| `connect.completed` | A Connect account link completes |

> **All 28 documented types** (the list may not be exhaustive; Speed may add more): [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speed-webhooks/references/overview.md).

## Delivery Semantics

* Reply `2xx` quickly. Any other status is retried: immediately, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h (about 5 days of retries), then the endpoint is deactivated. Missing headers or a bad signature: return `400` (or `401`).
* Each retry is re-signed with a new `webhook-id` and timestamp. Speed does not store event data it sends: capture it.
* No challenge or validation request is documented.

## Environment Variables

```bash
SPEED_WEBHOOK_SECRET=wsec_xxxxx   # the value exactly as Speed shows it; handlers fail closed (500) without it

```

One secret per endpoint: reveal it under **Developers > Webhooks** in the Speed dashboard, or read `secret` from Get Webhook Endpoint By Id.

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 speed --path /webhooks/speed

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speed-webhooks/references/overview.md) - Envelope, all event types, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speed-webhooks/references/setup.md) - Dashboard and API setup, the wsec_ secret, Connect, testing
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/speed-webhooks/references/verification.md) - Library and manual verification, timestamp window, debugging