Integrations

Stripe webhooks

Point Stripe at a Hookie endpoint: Hookie checks Stripe-Signature before anything is stored, files each event by type, and forwards it to your service signed, retried and replayable.

Build this freeNo credit card · Free plan included

Set it up

  1. Create an endpoint. In the console, open a project and add a webhook endpoint (or create it over the API, the CLI or MCP). Copy its URL, https://app.hookie.ai/{workspace}/{project}/{webhook}.
  2. Point Stripe at it. In Stripe Dashboard → Developers → Webhooks → Add endpoint, paste the URL and pick the events you want, for example invoice.paid, customer.subscription.updated, checkout.session.completed.
  3. Turn on signature verification. Edit the endpoint, set Verification to Stripe, and paste the endpoint's signing secret (whsec_…), under Reveal on the Stripe endpoint. From then on a request without a valid Stripe-Signature gets a 401 and nothing is stored or counted.
  4. Forward it to your service. Add a destination: any HTTPS URL, Slack, S3-compatible storage, SQS or Pub/Sub. Each delivery is signed, retried for about a day, and replayable.

Suggested routing

Each endpoint files what it receives into one dataset, through its own criteria. The Stripe preset suggests these; leave the criteria empty to keep every event in one dataset instead.

EndpointCriteriaDataset
Invoicestype = invoice.paidbilling_invoices
Subscriptionstype = customer.subscription.updatedbilling_subscriptions

What Hookie checks

Stripe signs each request with a timestamp and an HMAC-SHA256 of the timestamp and the body. Hookie recomputes it with your secret, accepts any matching v1 (Stripe sends two while it rolls a secret), and refuses a signature more than 5 minutes old so a captured request cannot be replayed later.

An event the criteria do not match is still answered 201 and kept as a submission, marked filtered, so you can see what was turned away and why.

Verify what Hookie forwards

Your service should check Hookie's signature the same way Hookie checks Stripe's. Every delivery to an HTTPS destination carries Hookie-Signature, keyed with that destination's signing secret (whsec_…, which an owner or admin can reveal on the destination). It also carries the Standard Webhooks headers, so any Standard Webhooks library verifies it too.

import crypto from "node:crypto";

// Hookie-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
// keyed with your destination's signing secret (whsec_...). During a secret
// rotation it carries one v1 per secret, so accept the request if ANY matches.
export function verifyHookie(rawBody, header, secret, toleranceSeconds = 300) {
  if (!header) return false;
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || !signatures.length) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return signatures.some((s) => s.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

Wired into an Express receiver, with the raw body the signature covers:

import express from "express";
import { verifyHookie } from "./verify-hookie.js";

const app = express();

// The RAW body: a signature is over the exact bytes sent, not re-serialised JSON.
app.post("/hooks/hookie", express.raw({ type: "*/*" }), (req, res) => {
  const body = req.body.toString("utf8");
  if (!verifyHookie(body, req.get("Hookie-Signature"), process.env.HOOKIE_SIGNING_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(body);
  // Retries repeat Hookie-Delivery-Id; dedupe the same event on Hookie-Event-Id.
  console.log(req.get("Hookie-Event-Id"), event);
  res.sendStatus(200);
});

app.listen(3000);

Check Stripe's signature yourself

Hookie does this for you on ingest. If you also receive Stripe directly somewhere else, this is the same check in plain Node, with no dependencies:

import crypto from "node:crypto";

// Stripe-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
export function verifyStripe(rawBody, header, endpointSecret, toleranceSeconds = 300) {
  if (!header) return false;
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const signatures = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || !signatures.length) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
  const expected = crypto.createHmac("sha256", endpointSecret).update(`${t}.${rawBody}`).digest("hex");
  return signatures.some((s) => s.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
}

Next: the quickstart, pricing, or how Hookie compares with Hookdeck, Svix and Formspree. Create a free endpoint and send Stripe a test event.