Skip to content

Signature Verification

Verify webhook signatures for Stripe, GitHub, Shopify, Twilio, Slack, Paddle, Linear, Clerk, Discord, Vercel, GitLab, and Standard Webhooks using the SDK.

Updated Mar 2026

Overview

Most webhook providers sign payloads so you can verify authenticity before processing. The SDK includes verification functions for 13 providers (12 with cryptographic verification, plus SendGrid which uses IP allowlisting), plus a generic verifySignature that dispatches by provider name.

Generic verifier (works with any supported provider):

import { verifySignature } from "@webhooks-cc/sdk";

Provider-specific verifiers (when you know the provider at build time):

import {
  verifyStripeSignature,
  verifyGitHubSignature,
  verifyShopifySignature,
  verifyTwilioSignature,
  verifySlackSignature,
  verifyPaddleSignature,
  verifyLinearSignature,
  verifyClerkSignature,
  verifyDiscordSignature,
  verifyVercelSignature,
  verifyGitLabSignature,
  verifyStandardWebhookSignature,
} from "@webhooks-cc/sdk";

Provider reference

ProviderHeaderAlgorithmSecret format
Stripestripe-signatureHMAC-SHA256whsec_... string
GitHubx-hub-signature-256HMAC-SHA256Hex string
Shopifyx-shopify-hmac-sha256HMAC-SHA256 (base64)Hex string
Twiliox-twilio-signatureHMAC-SHA1Auth token
Slackx-slack-signature + x-slack-request-timestampHMAC-SHA256Signing secret
Paddlepaddle-signature (ts + h1)HMAC-SHA256Hex string
Linearlinear-signatureHMAC-SHA256Hex string
Clerkwebhook-signature (Standard Webhooks / Svix)HMAC-SHA256Svix signing secret
Discordx-signature-ed25519 + x-signature-timestampEd25519Hex public key
Vercelx-vercel-signatureHMAC-SHA1Hex string
GitLabx-gitlab-tokenToken comparisonRaw secret token
Standard Webhookswebhook-id + webhook-timestamp + webhook-signatureHMAC-SHA256whsec_... or raw

Stripe

Stripe uses a t=timestamp,v1=hash format in the stripe-signature header. The SDK parses the header, reconstructs the signed payload as {timestamp}.{body}, and compares the HMAC-SHA256 hash.

const result = await verifySignature(request, {
  provider: "stripe",
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
});

GitHub

GitHub sends a sha256=hash value in x-hub-signature-256. The SDK computes HMAC-SHA256 over the raw body and compares.

const result = await verifySignature(request, {
  provider: "github",
  secret: process.env.GITHUB_WEBHOOK_SECRET!,
});

Shopify

Shopify sends a base64-encoded HMAC-SHA256 hash in x-shopify-hmac-sha256. The SDK computes the hash over the raw body and compares the base64 output.

const result = await verifySignature(request, {
  provider: "shopify",
  secret: process.env.SHOPIFY_WEBHOOK_SECRET!,
});

Twilio

Twilio verification requires the full signed URL -- the callback URL your endpoint was configured at, not just the request body. Pass it as the url option.

Twilio signs using HMAC-SHA1 over the full callback URL concatenated with sorted form parameters. The signature is base64-encoded and sent in x-twilio-signature.

const result = await verifySignature(request, {
  provider: "twilio",
  secret: process.env.TWILIO_AUTH_TOKEN!,
  url: "https://example.com/api/twilio/webhook",
});

Without the url option, verification throws an error.

Slack

Slack sends two headers: x-slack-signature (format v0=hash) and x-slack-request-timestamp. The SDK constructs the base string as v0:{timestamp}:{body} and computes HMAC-SHA256.

Slack rejects signatures when the timestamp is older than 5 minutes, so verify promptly after capture.

const result = await verifySignature(request, {
  provider: "slack",
  secret: process.env.SLACK_SIGNING_SECRET!,
});

Paddle

Paddle uses a two-part signature format: ts=timestamp;h1=hash in the paddle-signature header. The SDK parses both parts, reconstructs the signed payload as {timestamp}:{body}, and compares the HMAC-SHA256 hash.

const result = await verifySignature(request, {
  provider: "paddle",
  secret: process.env.PADDLE_WEBHOOK_SECRET!,
});

Linear

Linear sends an HMAC-SHA256 hash in the linear-signature header, optionally prefixed with sha256=. The SDK handles both formats.

const result = await verifySignature(request, {
  provider: "linear",
  secret: process.env.LINEAR_WEBHOOK_SECRET!,
});

Clerk

Clerk uses the Standard Webhooks (Svix) signing format. The SDK provides a dedicated verifyClerkSignature that delegates to Standard Webhooks verification using the Clerk-specific headers.

const result = await verifySignature(request, {
  provider: "clerk",
  secret: process.env.CLERK_WEBHOOK_SECRET!,
});

Vercel

Vercel signs deploy hook payloads with HMAC-SHA1 and sends the hex-encoded signature in x-vercel-signature.

const result = await verifySignature(request, {
  provider: "vercel",
  secret: process.env.VERCEL_WEBHOOK_SECRET!,
});

GitLab

GitLab uses raw token comparison rather than HMAC. The token you configure in GitLab's webhook settings is sent verbatim in the x-gitlab-token header. The SDK compares the header value against your secret using constant-time comparison.

const result = await verifySignature(request, {
  provider: "gitlab",
  secret: process.env.GITLAB_WEBHOOK_TOKEN!,
});

Discord

Discord uses Ed25519 public-key verification, not HMAC. Pass publicKey instead of secret. The key is your Discord application's hex-encoded public key.

Discord sends x-signature-ed25519 (hex-encoded signature) and x-signature-timestamp. The SDK verifies using crypto.subtle.verify with Ed25519.

const result = await verifySignature(request, {
  provider: "discord",
  publicKey: process.env.DISCORD_PUBLIC_KEY!,
});

Standard Webhooks

Standard Webhooks is used by Polar, Svix, Resend, Liveblocks, and Novu. It sends three headers: webhook-id, webhook-timestamp, and webhook-signature. The signed payload is {messageId}.{timestamp}.{body}, hashed with HMAC-SHA256 and base64-encoded. Clerk also uses Standard Webhooks signing but has its own dedicated verifyClerkSignature function and "clerk" provider name.

Secrets can include the whsec_ prefix (base64-encoded key after the prefix) or be passed as raw base64.

const result = await verifySignature(request, {
  provider: "standard-webhooks",
  secret: process.env.POLAR_WEBHOOK_SECRET!,
});

Generic verifier

verifySignature accepts a captured request and dispatches to the correct provider-specific function. It returns { valid: boolean }.

import { verifySignature } from "@webhooks-cc/sdk";
 
const result = await verifySignature(request, {
  provider: "stripe",
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
});
 
if (result.valid) {
  // Signature verified — safe to process
}

The options type is a discriminated union:

// HMAC/token-based providers
{
  provider: "stripe" | "github" | "shopify" | "twilio" | "slack" | "paddle" | "linear" | "sendgrid" | "clerk" | "vercel" | "gitlab" | "standard-webhooks";
  secret: string;
  url?: string;    // Required for twilio
}
 
// Ed25519-based provider (discord)
{
  provider: "discord";
  publicKey: string;
}

Using with captured requests

Verify a request captured by webhooks.cc. Headers and body are preserved exactly as received, so signatures remain valid.

const request = await client.requests.get(requestId);
 
const result = await verifySignature(request, {
  provider: "stripe",
  secret: process.env.STRIPE_WEBHOOK_SECRET!,
});
 
console.log(result.valid); // true

You can also use provider-specific functions directly. These accept the raw body, signature header, and secret as separate arguments:

import { verifyStripeSignature } from "@webhooks-cc/sdk";
 
const request = await client.requests.get(requestId);
const valid = await verifyStripeSignature(
  request.body,
  request.headers["stripe-signature"],
  process.env.STRIPE_WEBHOOK_SECRET!
);

FAQ

Learn more

Matchers & Helpers

Filter and parse webhook requests.

Testing

CI/CD integration and testing patterns.

API Reference

Complete method reference for the SDK.