Skip to content

Standard Webhooks + Vitest

Test Standard Webhooks handlers (Polar, Svix, Clerk, Resend) locally using @webhooks-cc/sdk sendTo with signed payloads and Vitest assertions.

Updated Mar 2026

Test webhook handlers for services that use the Standard Webhooks spec — Polar.sh, Svix, Clerk, Resend, and others. Uses sendTo to send signed payloads directly to localhost.

Test a Polar webhook handler

import { WebhooksCC } from "@webhooks-cc/sdk";
import { describe, it, expect } from "vitest";
 
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
 
describe("polar webhook handler", () => {
  it("processes subscription.created", 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",
        timestamp: new Date().toISOString(),
        data: {
          id: "sub_test_123",
          status: "active",
          customer: {
            id: "cust_1",
            email: "[email protected]",
            name: "Test User",
          },
          recurring_interval: "month",
          current_period_start: "2026-03-08T00:00:00Z",
          current_period_end: "2026-04-08T00:00:00Z",
        },
      },
    });
 
    expect(res.status).toBe(200);
  });
 
  it("processes subscription.canceled", async () => {
    const res = await client.sendTo("http://localhost:3000/api/webhooks/polar", {
      provider: "standard-webhooks",
      secret: process.env.POLAR_WEBHOOK_SECRET!,
      body: {
        type: "subscription.canceled",
        timestamp: new Date().toISOString(),
        data: {
          id: "sub_test_123",
          status: "canceled",
          customer: {
            id: "cust_1",
            email: "[email protected]",
            name: "Test User",
          },
        },
      },
    });
 
    expect(res.status).toBe(200);
  });
 
  it("rejects invalid signatures", async () => {
    const res = await client.sendTo("http://localhost:3000/api/webhooks/polar", {
      provider: "standard-webhooks",
      secret: "d3Jvbmctc2VjcmV0", // wrong secret (base64 of "wrong-secret")
      body: { type: "subscription.created", data: {} },
    });
 
    // Your handler should reject invalid signatures
    expect(res.status).toBe(401);
  });
});

How Standard Webhooks signing works

The SDK generates three headers per the Standard Webhooks spec:

HeaderValue
webhook-idUnique message ID (e.g. msg_a1b2c3...)
webhook-timestampUnix timestamp in seconds
webhook-signaturev1,<base64(HMAC-SHA256(secret, msgId.timestamp.body))>

Secrets with a whsec_ prefix are handled automatically. The SDK auto-detects whether the secret is base64-encoded (Svix, Clerk) or raw UTF-8 (Polar.sh) after stripping the prefix.

Body timestamp field

Many Standard Webhooks providers (including Polar.sh) require a timestamp ISO-8601 field in the payload body in addition to the webhook-timestamp header. The SDK generates the header automatically but you must include the body field yourself:

body: {
  type: "subscription.created",
  timestamp: new Date().toISOString(),  // required by Polar
  data: { /* ... */ },
}

Capture and inspect with webhooks.cc

You can also send Standard Webhooks to a webhooks.cc endpoint using sendTemplate, then inspect the headers and signature:

import { WebhooksCC, isStandardWebhook, matchHeader } from "@webhooks-cc/sdk";
 
const client = new WebhooksCC({ apiKey: process.env.WHK_API_KEY! });
 
describe("standard webhooks inspection", () => {
  let endpoint: Awaited<ReturnType<typeof client.endpoints.create>>;
 
  beforeAll(async () => {
    endpoint = await client.endpoints.create({ name: "stdwhk-test" });
  });
 
  afterAll(async () => {
    await client.endpoints.delete(endpoint.slug);
  });
 
  it("sends and captures a signed Standard Webhook", async () => {
    await client.endpoints.sendTemplate(endpoint.slug, {
      provider: "standard-webhooks",
      secret: process.env.POLAR_WEBHOOK_SECRET!,
      event: "subscription.created",
      body: { type: "subscription.created", data: { id: "sub_1" } },
    });
 
    const req = await client.requests.waitFor(endpoint.slug, {
      timeout: "10s",
      match: matchHeader("webhook-signature"),
    });
 
    expect(isStandardWebhook(req)).toBe(true);
    expect(req.headers["webhook-id"]).toMatch(/^msg_subscription\.created_/);
    expect(req.headers["webhook-signature"]).toMatch(/^v1,/);
 
    const body = JSON.parse(req.body!);
    expect(body.type).toBe("subscription.created");
  });
});

Deterministic signatures for snapshots

Pass a fixed timestamp to generate deterministic signatures, useful for snapshot testing:

const res = await client.sendTo("http://localhost:3000/api/webhooks", {
  provider: "standard-webhooks",
  secret: "dGVzdC1zZWNyZXQ=", // base64("test-secret")
  timestamp: 1700000000, // fixed timestamp
  body: { type: "test.event", data: {} },
});

Services using Standard Webhooks

ServiceSecret formatNotes
Polar.shwhsec_<raw-string>Secret is raw UTF-8, not base64 after prefix
Svixwhsec_<base64>Spec-compliant base64 after prefix
Clerkwhsec_<base64>Spec-compliant
Resendwhsec_<base64>Spec-compliant
Liveblockswhsec_<base64>Spec-compliant
Novuwhsec_<base64>Spec-compliant

The SDK auto-detects the format. If the part after whsec_ isn't valid base64, it falls back to raw UTF-8 bytes.

More examples

Stripe + Vitest

Payment webhook assertions.

GitHub + Jest

Push event verification.

Polar.sh + Playwright

Subscription lifecycle E2E testing.

Playwright E2E

Browser flow + webhook assertions.