Testing
Use the SDK in your test suite to verify webhook integrations and emails end-to-end. Create temporary endpoints, trigger your application, and assert on the captured requests and emails.
Updated Oct 2026
Pattern: test, assert, cleanup
import { WebhooksCC } from "@webhooks-cc/sdk";
import { describe, it, expect, beforeAll, afterAll } from "vitest";
const client = new WebhooksCC({
apiKey: process.env.WHK_API_KEY!,
});
describe("payment webhooks", () => {
let endpoint: Awaited<ReturnType<typeof client.endpoints.create>>;
beforeAll(async () => {
endpoint = await client.endpoints.create({
name: "test-payments",
});
});
afterAll(async () => {
await client.endpoints.delete(endpoint.slug);
});
it("sends a webhook on successful payment", async () => {
// Trigger your application with the endpoint URL
await processPayment({
webhookUrl: endpoint.url,
amount: 4999,
});
// Wait for the webhook to arrive
const request = await client.requests.waitFor(endpoint.slug, {
timeout: 5000,
match: (r) => r.method === "POST",
});
const body = JSON.parse(request.body!);
expect(body.event).toBe("payment.success");
expect(body.amount).toBe(4999);
});
});CI/CD integration
Add your API key as a secret in your CI environment. Example GitHub Actions config:
# .github/workflows/test.yml
env:
WHK_API_KEY: ${{ secrets.WHK_API_KEY }}
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm testUsing waitFor
The SDK includes a built-in waitFor method that polls until a matching request arrives:
// Wait for any request
const req = await client.requests.waitFor(endpoint.slug);
// Wait for a POST with a specific body
const req = await client.requests.waitFor(endpoint.slug, {
timeout: 10000,
pollInterval: 500,
match: (r) => {
if (r.method !== "POST") return false;
const body = JSON.parse(r.body ?? "{}");
return body.event === "order.created";
},
});Testing localhost webhook handlers
Use sendTo to send signed webhooks directly to your local server. No webhooks.cc endpoint is needed: the request goes straight to localhost with proper provider signatures.
import { WebhooksCC } from "@webhooks-cc/sdk";
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
it("handles Polar subscription webhook", async () => {
const res = await client.sendTo("http://localhost:3000/api/webhooks/polar", {
provider: "standard-webhooks",
secret: process.env.POLAR_WEBHOOK_SECRET!,
body: {
type: "subscription.created",
data: {
id: "sub_test_123",
status: "active",
customer: { email: "[email protected]" },
},
},
});
expect(res.status).toBe(200);
});Works with all providers: standard-webhooks (Polar, Svix, Clerk, Resend), stripe, github, shopify, twilio.
Testing utilities
The SDK provides testing helpers via the @webhooks-cc/sdk/testing subpath export:
import {
withEndpoint,
captureDuring,
captureEmailDuring,
assertRequest,
} from "@webhooks-cc/sdk/testing";
// Auto-cleanup wrapper: the endpoint is deleted when the callback completes
const request = await withEndpoint(client, async (endpoint) => {
await triggerWebhook(endpoint.url);
return client.requests.waitFor(endpoint.slug, { timeout: "10s" });
});
// Capture requests during an async action
const requests = await captureDuring(
client,
async (endpoint) => {
await triggerMultipleWebhooks(endpoint.url);
},
{ count: 3, timeout: "15s" }
);
// Structured assertion with diff output
const result = assertRequest(request, {
method: "POST",
bodyJson: { event: "payment.success" },
});
expect(result.pass).toBe(true);
// Capture the email an action triggers (see Testing emails below)
const [email] = await captureEmailDuring(client, async (address) => {
await signUp({ email: address });
});Testing emails
Every endpoint on an account also receives email, at <slug>@mailhooks.cc and at <slug>+<tag>@mailhooks.cc for any tag. Point your signup, login, or password reset flow at that address, wait for the email, and assert on it. Each email counts as one request.
captureEmailDuring
captureEmailDuring creates a temporary endpoint, runs your action with its email address, waits for the email it triggers, and deletes the endpoint, even when the test fails:
import { WebhooksCC, extractCode, extractLink } from "@webhooks-cc/sdk";
import { captureEmailDuring } from "@webhooks-cc/sdk/testing";
import { expect, it } from "vitest";
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
it("sends a confirmation email on signup", async () => {
const [email] = await captureEmailDuring(
client,
async (address) => {
await signUp({ email: address, password: "correct horse battery staple" });
},
{ subject: "Confirm your email", timeout: "30s" }
);
expect(email.email.from[0]?.address).toBe("[email protected]");
expect(extractCode(email)).toMatch(/^\d{6}$/);
expect(extractLink(email)).toContain("/confirm?token=");
});The action gets the address and the endpoint. The options take the email criteria (tag, subject, from, to), count (default: 1), timeout (default: 60 seconds), pollInterval (default: 1 second), a match function, and the options of endpoints.create(). With tag, the action gets the tagged address and only mail to that tag counts. Every email the new endpoint received counts, so nothing from an earlier run can match.
One endpoint, a tag per test
To keep one endpoint for a whole suite, give each test its own tag. Mail to a tagged address lands on the same endpoint, and emails.waitFor only takes mail with that tag:
import { WebhooksCC, extractLink } from "@webhooks-cc/sdk";
import { afterAll, beforeAll, expect, it } from "vitest";
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
let slug: string;
beforeAll(async () => {
slug = (await client.endpoints.create({ name: "auth-emails" })).slug;
});
afterAll(async () => {
await client.endpoints.delete(slug);
});
it("sends a password reset link", async () => {
const tag = `reset-${Date.now()}`;
await requestPasswordReset(client.emails.address(slug, tag));
const email = await client.emails.waitFor(slug, { tag, timeout: "30s" });
expect(email.email.subject).toContain("Reset your password");
expect(extractLink(email)).toMatch(/\/reset\?token=/);
});To check the wiring before your app sends anything, client.emails.sendTest(slug, { tag }) delivers a sample email with a six-digit code.
To test the server that receives forwarded emails, post a captured email to it with client.sendTo(); see Test your handler locally.
Tips
- Use unique endpoint names per test run to avoid conflicts in parallel CI
- Always clean up endpoints in
afterAll/afterEach - The free plan supports 50 requests a day, enough for most test suites; each email counts as one request
- Use a fresh
+tagper test run when several runs share an endpoint's email address
Framework examples
Standard Webhooks + Vitest
Polar, Svix, Clerk, Resend with sendTo.
Stripe + Vitest
Payment webhook assertions.
GitHub + Jest
Push event verification.
Polar.sh + Playwright
Subscription lifecycle E2E testing.
Playwright E2E
Browser flow + webhook assertions.