# beehiiv Webhooks

[beehiiv](https://www.beehiiv.com) is a newsletter platform. Its webhooks report subscriber, post, survey and podcast activity. beehiiv delivers them through **Svix**, so every request carries `svix-id`, `svix-timestamp` and `svix-signature` headers that you verify with your endpoint's signing secret. Webhooks need a paid beehiiv plan (Lite and above).

## When to Use This Skill

* How do I receive beehiiv webhooks?
* How do I verify a beehiiv webhook signature (`svix-signature`)?
* Where do I find my beehiiv webhook signing secret?
* How do I handle `subscription.created`, `subscription.upgraded` or `post.sent` events?
* Why is my beehiiv webhook signature verification failing?
* Why are Hookdeck retries of beehiiv webhooks rejected after a few minutes?

## Verification (core)

| Header | Value |
| --- | --- |
| `svix-id` | Message ID, the same on every retry |
| `svix-timestamp` | Unix seconds when the attempt was sent |
| `svix-signature` | One or more space-separated `v1,<base64>` entries |

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`, HMAC-SHA256, base64. The key is the base64-**decoded** part of the endpoint secret after `whsec_`. The secret is the endpoint's signing secret from **Settings > Webhooks** (`BEEHIIV_WEBHOOK_SECRET`). Verify with the `svix` library (npm `svix`, PyPI `svix`):

```javascript
import { Webhook } from 'svix';

// rawBody: the exact request body string, NOT re-serialised JSON
function verifyBeehiivWebhook(rawBody, headers, secret = process.env.BEEHIIV_WEBHOOK_SECRET) {
  new Webhook(secret).verify(rawBody, {
    'svix-id': headers['svix-id'],
    'svix-timestamp': headers['svix-timestamp'],
    'svix-signature': headers['svix-signature'],
  }); // throws on a bad signature or stale timestamp; respond 400
  return JSON.parse(rawBody); // parse only after verification
}

```

Python: `from svix.webhooks import Webhook`, then `Webhook(secret).verify(raw_body, headers)` (raises `WebhookVerificationError`).

> **Retries and replays:** the `svix` library always enforces a fixed 5-minute timestamp window. Hookdeck retries and replays resend the **original** `svix-timestamp`, so the library rejects them once they are more than 5 minutes old. If you retry or replay late, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/beehiiv-webhooks/references/verification.md), where the window can be dropped or widened.

Missing headers or a bad signature: respond `400` (or `401`). In Express, read the raw body with `express.raw({ type: '*/*' })` so any content type verifies.

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

## Event Envelope

```json
{ "uid": "evt_...", "event_type": "subscription.created",
  "event_timestamp": 1792922400, "data": { } }

```

Dispatch on `event_type`. `uid` identifies the event; the `svix-id` header is stable across retries and works as a dedupe key too.

## Common Event Types

| Event | Fired when |
| --- | --- |
| `post.sent` | A post is sent |
| `post.updated` | A post is updated |
| `post.scheduled` | A post is scheduled |
| `subscription.created` | A subscription is created |
| `subscription.confirmed` | A subscriber confirms their subscription |
| `subscription.deleted` | A subscription is deleted |
| `subscription.upgraded` / `subscription.downgraded` | A subscriber moves to a higher / lower tier |
| `subscription.paused` / `subscription.resumed` | A subscription is paused / resumed |
| `newsletter_list_subscription.subscribed` | A subscriber joins a newsletter list |
| `survey.response_submitted` | A survey response is submitted |
| `podcasts.episode.published` | A podcast episode is published |

beehiiv lists more event types; see [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/beehiiv-webhooks/references/overview.md) and the [beehiiv webhook docs](https://developers.beehiiv.com/webhooks).

## Environment Variables

```bash
BEEHIIV_WEBHOOK_SECRET=whsec_xxxxx   # endpoint signing secret; handlers fail closed (500) without it

```

## Local Development

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

```

Add the printed URL as an endpoint under **Settings > Webhooks** in beehiiv.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/beehiiv-webhooks/references/overview.md) - Event catalog and payload envelope
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/beehiiv-webhooks/references/setup.md) - Create an endpoint and find the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/beehiiv-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging