Guides

Webhooks

FlockStack can tell your own server, a CRM or an automation app when something happens: someone subscribes or leaves, a campaign goes out, an order is paid. Each event is a signed JSON POST to an address you choose, retried until it gets through.

Add an endpoint

On the Webhooks page, or with create_webhook_endpoint, give the address, and optionally the events it takes and a newsletter. It gets every event when you pick none, new kinds included, and every newsletter's when you pick none. The answer has the signing secret, shown once.

The address must be https:// on the default port, at a public address: not localhost, a private network or a cloud metadata address. Redirects aren't followed, so give the final address. Webhooks are on every plan, with this many endpoints: Starter 3, Growth 5, Pro 10, Scale 20, Max 50. Each Zapier or Make trigger uses one. A self-hosted server has no limit.

Terminal
curl https://flockstack.io/api/v1/webhooks \
  -H "Authorization: Bearer $FLOCKSTACK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/flockstack-events","events":["subscriber.created","subscriber.unsubscribed"]}'

Events

  • subscriber.created Someone signed up, was added or came back after unsubscribing. status is pending until they confirm, on a newsletter that asks.
  • subscriber.confirmed A pending subscriber clicked the link in the confirmation email.
  • subscriber.unsubscribed A subscriber unsubscribed: from a link in an email, their mail app's button, or on the dashboard or API.
  • subscriber.updated A subscriber's tags or custom fields changed. changes says which.
  • subscriber.upgraded A reader started a paid subscription, or their trial began.
  • subscriber.downgraded A paid reader's subscription ended or stopped paying: they get free issues from now on.
  • campaign.sent A campaign finished going out. A campaign sent in steps sends this after each step.
  • order.paid A reader paid for a digital product.

Subscriber events carry the subscriber with the same fields, by the same names, as the API's contacts. Contacts added by an import send no events, so a large import doesn't flood your endpoint. webhook.test is only sent when you ask for a test event.

What a delivery looks like

A POST with a JSON body: the event's type, when it happened, and its data. Answer with any 2xx status within 15 seconds; do slow work after answering.

Headers
POST /flockstack-events HTTP/1.1
content-type: application/json
user-agent: FlockStack-Webhooks/1.0 (+https://flockstack.io/docs/webhooks)
webhook-id: msg_6650f1c2e9b1d4a0123456ee
webhook-timestamp: 1760952600
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
Body
{
  "type": "subscriber.created",
  "timestamp": "2026-10-20T09:30:00.000Z",
  "data": {
    "subscriber": {
      "id": "6650f1c2e9b1d4a012345678",
      "newsletterId": "6650f1c2e9b1d4a0123456aa",
      "email": "jane@example.com",
      "status": "active",
      "source": "website",
      "tier": "free",
      "paidUntil": null,
      "tags": [
        "podcast"
      ],
      "fields": {
        "first_name": "Jane"
      },
      "subscribedAt": "2026-10-20T09:30:00.000Z",
      "confirmedAt": null,
      "unsubscribedAt": null
    },
    "resubscribed": false
  }
}

Check the signature

Events are signed the Standard Webhooks way, so any of its libraries verifies them with your secret. By hand: compute HMAC-SHA256 of webhook-id.webhook-timestamp.body, keyed with the base64 after whsec_, and compare it with each v1, value of webhook-signature. Refuse events whose timestamp is more than 5 minutes away.

verify.js
// Node.js 18 or later, no packages needed. Use the raw body: parsing and
// re-serializing the JSON changes it, and the signature stops matching.
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.FLOCKSTACK_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 5 * 60;

export function verifyFlockStackEvent(rawBody, headers) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"] ?? "";
  if (!id || !timestamp) return false;
  // Refuse old events, so a captured request can't be replayed later.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;

  const key = Buffer.from(SECRET.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
  // While you rotate the secret, events carry two signatures: one is enough.
  return signatures.split(" ").some((part) => {
    const [version, value] = part.split(",");
    if (version !== "v1" || !value) return false;
    const received = Buffer.from(value, "base64");
    return received.length === expected.length && timingSafeEqual(received, expected);
  });
}

// Express: keep the body as text for this route.
app.post("/flockstack-events", express.text({ type: "application/json" }), (req, res) => {
  if (!verifyFlockStackEvent(req.body, req.headers)) return res.status(401).end();
  const event = JSON.parse(req.body);
  // event.type, event.timestamp, event.data. Use webhook-id to skip repeats.
  res.status(204).end();
});

rotate_webhook_secret makes a new secret. For 24 hours events carry two signatures, one with each secret, so you can change your server without missing any.

Retries and failures

An answer other than 2xx, a redirect, or no answer in 15 seconds is a failure. FlockStack tries again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, then once a day: 11 attempts over about 5 days. Every attempt has the same webhook-id, so you can tell a repeat from a new event. Events can arrive out of order.

An endpoint that hasn't accepted an event through those 5 days is turned off, and the account's owner gets an email. So is one that answers 410 Gone. Fix it, send a test event, and turn it back on. Events from while it was off aren't sent on their own: resend the ones you need from its log (resend_webhook_delivery).

Each endpoint's log keeps its latest 100 deliveries with the body sent, the attempts and the last answer (list_webhook_deliveries).

Zapier and Make

The Zapier and Make apps use these webhooks for their triggers: turning a Zap or a scenario on adds an endpoint, turning it off deletes it.