Gareth Wilson Gareth Wilson

Guide to beehiiv Webhooks: Features and Best Practices

Published


Webhooks from beehiiv, the newsletter platform, tell your systems when readers subscribe, confirm, upgrade, pause or leave, when a post is scheduled or sent, when a survey response comes in, and when a podcast episode is published. They are how new subscribers reach your CRM, paid upgrades reach your billing or access control, and sent posts reach your social or analytics tools without polling the API.

This guide covers how beehiiv webhook endpoints are created and signed, the events and their payloads, what beehiiv doesn't document, and the practices that keep a beehiiv receiver reliable.

What are beehiiv webhooks?

A beehiiv webhook is an endpoint URL on a publication plus the event types it should receive. When one of those events happens, beehiiv sends a JSON POST to the URL. Webhooks are only available on paid plans, Lite and above.

The delivery runs through Svix. In beehiiv's words, each request "includes svix-id, svix-timestamp, and svix-signature headers that you should verify with your endpoint's signing secret before processing the payload". The signing secret is on the endpoint's details page in the app.

beehiiv webhook features

FeatureDetails
AvailabilityPaid plans, Lite and above
ConfigurationSettings > Webhooks, or POST /v2/publications/{publicationId}/webhooks
Subscriptionevent_types per endpoint
Payload{ "uid", "event_type", "event_timestamp", "data" }
Signature headerssvix-id, svix-timestamp, svix-signature
SchemeSvix (Standard Webhooks): HMAC-SHA256, base64, entries prefixed with v1,
Signed content{svix-id}.{svix-timestamp}.{raw body}
SecretThe endpoint's signing secret, shown in the app on the endpoint details page
No-code optionsbeehiiv's own Zapier and Make integrations
Retries, timeout and source IPsNot published

Common events

The Create webhook API accepts 26 event types:

AreaEvent types
Postspost.sent, post.updated, post.scheduled
Subscriptionssubscription.created, subscription.confirmed, subscription.deleted, subscription.upgraded, subscription.downgraded, subscription.paused, subscription.resumed
Subscription tierssubscription.tier.created, subscription.tier.deleted, subscription.tier.paused, subscription.tier.resumed
Newsletter listsnewsletter_list_subscription.subscribed, newsletter_list_subscription.unsubscribed, newsletter_list_subscription.paused, newsletter_list_subscription.resumed
Surveyssurvey.response_submitted
Podcastspodcasts.episode.published, podcasts.episode.updated, podcasts.episode.archived, podcasts.episode.deleted, podcasts.private_feed.activated, podcasts.private_feed.access_revoked, podcasts.private_feed.deactivated

Every payload has the same four top-level fields:

FieldDescription
uidThe prefixed event ID, unique to each webhook event
event_typeThe event type. Route on it
event_timestampWhen the event was created, in seconds since the Unix epoch
dataThe resource the event is about

For subscription events, data is a subscription: id, email, status (validating, invalid, pending, active, inactive, needs_attention, paused or unidentified), created, subscription_tier, subscription_premium_tier_names, referral_code, referring_site, stripe_customer_id, the utm_* acquisition fields, custom_fields and tags. For post events, it is a post with id, title, subject_line, slug, status, platform, authors, content_tags and other display fields.

beehiiv documents each event as a field schema rather than an example body, and marks most fields as optional. Log a real delivery before relying on a field being present.

Setting up beehiiv webhooks

In the app

  1. Go to Settings > Webhooks for your publication.
  2. Create an endpoint with your HTTPS URL and choose the event types.
  3. Open the endpoint's details page and copy the signing secret. Store it server-side as BEEHIIV_WEBHOOK_SECRET.

Through the API

curl -X POST "https://api.beehiiv.com/v2/publications/$BEEHIIV_PUBLICATION_ID/webhooks" \
  -H "Authorization: Bearer $BEEHIIV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/beehiiv",
    "event_types": ["subscription.created", "subscription.upgraded", "post.sent"],
    "description": "CRM sync"
  }'

OAuth apps need the webhooks:write scope. The API doesn't return the signing secret, so read it from the endpoint details page in the app afterwards.

With Hookdeck. Create a source with the beehiiv source type, paste the signing secret into Webhook Signing Secret exactly as the app shows it, and use the Hookdeck source URL as the endpoint URL. Svix secrets start with whsec_. Hookdeck decodes a whsec_ secret as Svix does, and treats a secret without the prefix as plain text, which wouldn't match. A delivery that fails the check still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

Securing beehiiv webhooks

beehiiv points to Svix's manual verification steps. Join svix-id, svix-timestamp and the raw request body with dots. Take the part of the secret after whsec_, base64-decode it, and use the bytes as the HMAC-SHA256 key. Encode the result as base64 and compare it in constant time with each v1, entry in the space-delimited svix-signature header. A match on any entry is enough.

beehiiv names no timestamp tolerance. The check below is optional and off by default. If you turn it on, remember that a delivery retried or replayed later, for example from Hookdeck, keeps its original timestamp.

const crypto = require('crypto');
const express = require('express');

function verifyBeehiivSignature(rawBody, headers, secret, toleranceSeconds = 0) {
  const id = headers['svix-id'];
  const timestamp = headers['svix-timestamp'];
  const signatureHeader = headers['svix-signature'];
  if (!id || !timestamp || !signatureHeader || !secret || !secret.startsWith('whsec_')) return false;
  if (!/^\d+$/.test(timestamp)) return false;

  // Optional replay check, off by default.
  if (toleranceSeconds > 0 && Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) {
    return false;
  }

  const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.`)
    .update(rawBody)
    .digest();

  return signatureHeader.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const received = Buffer.from(signature, 'base64');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

const app = express();

app.post('/webhooks/beehiiv', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyBeehiivSignature(req.body, req.headers, process.env.BEEHIIV_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.uid (or the svix-id header), then route on event.event_type.
  res.sendStatus(200);
});

The same check in Python:

import base64
import binascii
import hashlib
import hmac
import time


def verify_beehiiv_signature(raw_body: bytes, headers, secret: str, tolerance_seconds: int = 0) -> bool:
    msg_id = headers.get("svix-id")
    timestamp = headers.get("svix-timestamp")
    signature_header = headers.get("svix-signature")
    if not (msg_id and timestamp and signature_header and secret and secret.startswith("whsec_")):
        return False
    if not timestamp.isdigit():
        return False
    # Optional replay check, off by default.
    if tolerance_seconds > 0 and abs(time.time() - int(timestamp)) > tolerance_seconds:
        return False

    try:
        key = base64.b64decode(secret[len("whsec_"):])
    except (binascii.Error, ValueError):
        return False
    signed = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())

    for entry in signature_header.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
            return True
    return False


# In FastAPI: raw_body = await request.body()
# verify_beehiiv_signature(raw_body, request.headers, os.environ["BEEHIIV_WEBHOOK_SECRET"])

The svix libraries do the same check but always enforce a five-minute timestamp window. A delivery that Hookdeck retries or replays more than five minutes after beehiiv sent it fails the library's check even though its signature is valid. The beehiiv webhook skill includes a manual verifier where you can drop or widen the window.

Make beehiiv webhooks production-ready. Hookdeck Event Gateway verifies the Svix signature on every beehiiv delivery, stores each subscriber and post event, and delivers it to your CRM, billing and analytics services with retries and replay you control.

beehiiv webhook limitations and pain points

The Problem: The webhooks settings aren't there, or the API refuses to create one.

Why It Happens: beehiiv offers webhooks only to paid publications on the Lite plan and above.

Workarounds:

  • Upgrade the publication, or use beehiiv's Zapier or Make integration if it covers what you need.

How Hookdeck Can Help: Once webhooks are on, a single Hookdeck source can fan out each event to several services, so you don't need an endpoint per consumer.

Schemas, not examples

The Problem: Code written from the docs breaks on the first real delivery.

Why It Happens: beehiiv documents each event as a list of fields, most marked optional, with no example body.

Workarounds:

  • Send yourself real events, such as a test subscription, and build your parser from them.
  • Use optional access for every field in data.

How Hookdeck Can Help: Hookdeck stores each request with its headers and body, so you can capture one of each event type and replay them against your handler while you develop it.

No published retry policy

The Problem: You don't know whether beehiiv resends an event your endpoint missed, or for how long.

Why It Happens: beehiiv documents no timeout, retry schedule or failure handling for webhooks.

Workarounds:

  • Return a 2xx quickly and do the work afterwards.
  • Reconcile subscribers through beehiiv's API after an outage.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, then delivers to your service with automatic retries and lets you replay anything that failed.

Subscriber data in the payload

The Problem: Webhook bodies carry subscriber email addresses, custom fields and Stripe customer IDs.

Why It Happens: Subscription events include the subscription record in data.

Workarounds:

  • Verify every request, keep the endpoint on HTTPS, and avoid logging full bodies.
  • Subscribe each endpoint only to the events it needs.

How Hookdeck Can Help: Hookdeck's transformations can remove fields before an event reaches a destination that doesn't need them.

Best practices

Verify against the raw body

The signature covers the exact bytes beehiiv sent. Use express.raw({ type: '*/*' }) in Express, await request.text() in Next.js and await request.body() in FastAPI, and parse only after verification.

Deduplicate on uid

uid is unique to each webhook event, and svix-id stays the same when Svix resends a message. Store one of them before processing and skip repeats. See our guide to webhook idempotency.

Treat events as notifications for paid access

For upgrades, downgrades and pauses, re-read the subscription from the API before changing someone's access, so an out-of-order event can't undo a newer change.

Acknowledge fast and process asynchronously

Verify, store the event, return 200, and update downstream systems afterwards. See why to process webhooks asynchronously.

Conclusion

beehiiv delivers webhooks through Svix, with svix-* headers and a per-endpoint signing secret found on the endpoint details page. Verify against the raw body with the decoded key, route on event_type, deduplicate on uid, and build your parser from real deliveries, because the docs list fields but show no examples.

Hookdeck Event Gateway verifies each beehiiv signature and keeps every event until your service has processed it, so a new subscriber never goes missing because of an outage on your side.

Get started with Hookdeck for free and handle beehiiv webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.