Testing
Test webhook routes with signed requests and no crypto code.
next-webhooks/testing builds correctly signed headers for every built-in provider, so route tests call your handler exactly the way the provider would.
tests/stripe-webhook.test.ts
import { expect, it } from "vitest";
import { stripeHeaders } from "next-webhooks/testing";
import { POST } from "../app/api/webhooks/stripe/route";
it("handles invoice.paid", async () => {
const body = JSON.stringify({
id: "evt_1",
type: "invoice.paid",
data: { object: {} },
});
const res = await POST(
new Request("http://localhost/api/webhooks/stripe", {
method: "POST",
body,
headers: await stripeHeaders({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
body,
}),
}),
);
expect(res.status).toBe(200);
});Available builders
- stripeHeaders({ secret, body, timestamp? })
- githubHeaders({ secret, body, deliveryId?, event? })
- svixHeaders({ secret, body, id?, timestamp? })
- slackHeaders({ secret, body, timestamp? })
- paddleHeaders({ secret, body, timestamp? })
- shopifyHeaders({ secret, body, topic?, webhookId? })
- lemonsqueezyHeaders({ secret, body, event? })
- vercelHeaders({ secret, body })
- discordHeaders({ privateKey, body, timestamp? })
- hmacHeaders({ secret, body, header, algorithm?, encoding?, prefix? })
Testing Discord routes
Discord verifies against a public key instead of a shared secret, so tests generate a key pair: configure the provider with the public half and sign requests with the private half.
tests/discord-webhook.test.ts
import { discordKeys, discordHeaders } from "next-webhooks/testing";
import { webhook, discord } from "next-webhooks";
const { publicKey, privateKey } = await discordKeys();
const POST = webhook({
provider: discord({ publicKey }),
handler: async () => {},
});
const body = JSON.stringify({ id: "1", type: 2 });
const res = await POST(
new Request("http://localhost/api/webhooks/discord", {
method: "POST",
body,
headers: await discordHeaders({ privateKey, body }),
}),
);