# useSend Webhooks

useSend (usesend.com) is an open-source email sending platform. It sends a
webhook when an email is queued, sent, delivered, bounced or opened, when a
contact changes, and when a domain changes.

## When to Use This Skill

* How do I receive useSend webhooks?
* How do I verify the `X-UseSend-Signature` header?
* Why is my useSend signature verification failing?
* How do I handle `email.delivered`, `email.bounced` or `email.clicked` events?
* How do I set up a webhook in the useSend dashboard?

## How Verification Works

* Headers: `X-UseSend-Signature` (`v1=<hex>`) and `X-UseSend-Timestamp` (Unix
  **milliseconds**).
* Signature: lowercase hex HMAC-SHA256, keyed with the secret, over
  `{X-UseSend-Timestamp}` + `.` + the **raw request body bytes**.
* The secret is used **whole, as a plain UTF-8 key, including its `whsec_`
  prefix**. Nothing is base64-decoded.
* **This is not Standard Webhooks.** There is no `webhook-id` header and the
  digest is hex, so do not use the `standardwebhooks` or `svix` libraries.
* A missing or empty header, a malformed value or a mismatch is a `401`.

> **Setup note:** Paste the whole secret, including the whsec_ prefix. useSend uses it as a plain HMAC key.

## Verification (core)

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

// rawBody: Buffer of the exact bytes received (never parsed JSON).
function verifyUseSend(rawBody, signature, timestamp, secret) {
  if (!signature || !timestamp || !secret) return false;
  const hex = (signature.startsWith('v1=') ? signature.slice(3) : signature).toLowerCase();
  if (!/^[0-9a-f]{64}$/.test(hex)) return false; // validate before decoding
  const message = Buffer.concat([
    Buffer.from(timestamp, 'latin1'), Buffer.from('.'), rawBody,
  ]);
  const expected = crypto.createHmac('sha256', Buffer.from(secret, 'utf8')).update(message).digest();
  return crypto.timingSafeEqual(expected, Buffer.from(hex, 'hex'));
}

```

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

The examples verify by hand rather than with the `usesend-js` SDK, because the
SDK rejects timestamps older than 5 minutes by default and Hookdeck retries and
replays resend the original timestamp. See
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/usesend-webhooks/references/verification.md).

### Timestamp tolerance is opt-in

The examples do **not** reject old timestamps by default. Hookdeck retries and
replays resend the original headers, and Hookdeck itself applies no tolerance,
so a fixed window would reject legitimate retries. To turn one on, set
`USESEND_WEBHOOK_TOLERANCE_SECONDS` (unset or `0` = off). Remember the header is
in milliseconds. Deduplicate on the payload `id` either way.

## Common Event Types

Dispatch on the body's `type` field.

| Event | Triggered When |
| --- | --- |
| `email.delivered` | Email was successfully delivered |
| `email.bounced` | Email bounced (permanent or temporary); `data.bounce` has details |
| `email.complained` | Recipient marked the email as spam |
| `email.opened` / `email.clicked` | Recipient opened the email / clicked a link |
| `contact.created` | New contact was created |
| `domain.verified` | Domain verification completed |
| `webhook.test` | Sent by "Send Test" in the dashboard |

useSend documents 20 event types plus the `webhook.test` event (21 in all):
13 `email.*`, 3 `contact.*` and 4 `domain.*`. The examples handle a few and log
the rest. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/usesend-webhooks/references/overview.md).

## Environment Variables

| Variable | Description |
| --- | --- |
| `USESEND_WEBHOOK_SECRET` | The webhook signing secret, whole, including `whsec_` |
| `USESEND_WEBHOOK_TOLERANCE_SECONDS` | Optional replay window in seconds (unset or `0` = off) |

## Delivery Behaviour

POST with a JSON body. A `2xx` within 10 seconds acknowledges it. Otherwise
useSend makes 6 attempts in all (immediately, then after about 5, 10, 20, 40 and
80 seconds). A webhook is disabled after 30 consecutive failed calls and is
re-enabled from the dashboard. Duplicates are possible.

## Local Development

For local webhook testing, run the Hookdeck CLI via `npx` — no install required:

```bash
npx hookdeck-cli listen 3000 usesend --path /webhooks/usesend

```

No account required — the CLI creates a guest account on first run and provides a
local tunnel + web UI for inspecting requests.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/usesend-webhooks/references/overview.md) — What useSend webhooks are, events, payload structure
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/usesend-webhooks/references/setup.md) — Create the webhook, get the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/usesend-webhooks/references/verification.md) — Signature details and gotchas
* [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/usesend-webhooks/examples/express/) — Express (Node.js) handler + tests
* [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/usesend-webhooks/examples/nextjs/) — Next.js App Router handler + tests
* [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/usesend-webhooks/examples/fastapi/) — FastAPI (Python) handler + tests