webhook-verify-all
v1.0.3
Published
Unified webhook signature verification for 12+ providers (Stripe, Slack, Shopify, PayPal, Telegram, Discord, GitHub, GitLab, Twilio, SendGrid, Auth0, Unlimit) with TypeScript support and optional replay protection
Maintainers
Readme
webhook-verify-all
A professional, TypeScript-first npm package for verifying webhook signatures from major providers with a unified API and optional replay protection.
Features
- ✅ TypeScript-first - Full type safety with strict mode
- ✅ Multiple Providers - Stripe, Slack, Shopify, PayPal, Telegram, Discord, Unlimit, GitHub, GitLab, Twilio, SendGrid, Auth0
- ✅ Unified API - Consistent interface across all providers
- ✅ Replay Protection - Optional in-memory or Redis-based replay store
- ✅ Framework Support - Express and Fastify middleware helpers
- ✅ Security Best Practices - Constant-time comparison, timestamp validation, clock skew handling
- ✅ Zero External Crypto Dependencies - Uses Node.js native crypto only
- ✅ Production Ready - Comprehensive error handling, unit tests, and documentation
Why webhook-verify-all?
- 🚀 One package for all providers - No need to install separate packages for each webhook provider
- 🔒 Security-first - Constant-time comparison, replay protection, and timestamp validation built-in
- 📦 Zero dependencies - Uses only Node.js native crypto (except optional ioredis for Redis)
- 🎯 Type-safe - Full TypeScript support with strict mode
- ⚡ Framework agnostic - Works with Express, Fastify, or any Node.js framework
- 🛡️ Production tested - 109+ unit and integration tests
Installation
npm install webhook-verify-allFor Express or Fastify middleware, install Express or Fastify in your app:
# For Express
npm install webhook-verify-all express
# For Fastify
npm install webhook-verify-all fastifyFor Redis replay protection:
npm install webhook-verify-all ioredisQuick Start
Basic Usage
import { verifyWebhook } from "webhook-verify-all";
const result = await verifyWebhook("stripe", {
secret: process.env.STRIPE_WEBHOOK_SECRET!,
body: rawRequestBody,
signature: req.headers["stripe-signature"],
});
if (result.ok) {
// Webhook is verified, process it
console.log(`Verified ${result.provider} webhook`);
} else {
// Verification failed
console.error(`Error: ${result.error} (${result.errorCode})`);
}Note: The unified API (verifyWebhook) is the recommended approach. Provider-specific verifiers (e.g., verifyStripe) are also available for convenience.
Express Middleware
import express from "express";
import { webhookMiddleware } from "webhook-verify-all/express";
const app = express();
// Important: Parse raw body BEFORE the webhook middleware
// Do NOT use express.json() on the same route - use express.raw() instead
app.use("/webhook", express.raw({ type: "application/json" }));
app.post(
"/webhook",
webhookMiddleware({
provider: "stripe",
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
(req, res) => {
if (req.webhook?.ok) {
// Handle verified webhook
res.json({ received: true });
} else {
res.status(401).json({ error: req.webhook?.error });
}
}
);Fastify Middleware
import Fastify from "fastify";
import { webhookMiddleware } from "webhook-verify-all/fastify";
const fastify = Fastify();
fastify.post(
"/webhook",
{
preHandler: webhookMiddleware({
provider: "stripe",
secret: process.env.STRIPE_WEBHOOK_SECRET!,
}),
},
async (req, reply) => {
if (req.webhook?.ok) {
return { received: true };
} else {
reply.code(401);
return { error: req.webhook?.error };
}
}
);Supported Providers
Stripe
import { verifyStripe } from "webhook-verify-all";
const result = await verifyStripe({
secret: process.env.STRIPE_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["stripe-signature"]!,
timestampTolerance: 300, // 5 minutes
});Headers Required:
stripe-signature: Formatt=timestamp,v1=signature
Slack
import { verifySlack } from "webhook-verify-all";
const result = await verifySlack({
secret: process.env.SLACK_SIGNING_SECRET!,
body: rawBody,
signature: req.headers["x-slack-signature"]!,
timestamp: req.headers["x-slack-request-timestamp"]!,
});Headers Required:
x-slack-signature: Formatv0=signaturex-slack-request-timestamp: Unix timestamp
Shopify
import { verifyShopify } from "webhook-verify-all";
const result = await verifyShopify({
secret: process.env.SHOPIFY_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["x-shopify-hmac-sha256"]!,
});Headers Required:
x-shopify-hmac-sha256: HMAC-SHA256 signature (base64 encoded)
PayPal
import { verifyPayPal } from "webhook-verify-all";
const result = await verifyPayPal({
secret: process.env.PAYPAL_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["paypal-transmission-sig"]!,
});Headers Required:
paypal-transmission-sig: Signature header
Note: PayPal uses certificate/public-key verification (not simple HMAC). This implementation provides a simplified HMAC-based approach. For production use, consider PayPal's official SDK for full certificate verification with PayPal-Transmission-Id, PayPal-Transmission-Time, PayPal-Cert-Url, and PayPal-Auth-Algo headers.
Telegram
import { verifyTelegram } from "webhook-verify-all";
const result = await verifyTelegram({
secret: process.env.TELEGRAM_BOT_SECRET!,
body: rawBody,
signature: req.headers["x-telegram-bot-api-secret-token"]!,
});Headers Required:
x-telegram-bot-api-secret-token: Secret token (must match the secret you set viasetWebhook)
Note: Telegram uses simple token equality check (constant-time comparison), not HMAC. The token in the header must exactly match the secret you configured when setting up the webhook.
Discord
import { verifyDiscord } from "webhook-verify-all";
const result = await verifyDiscord({
secret: process.env.DISCORD_PUBLIC_KEY!, // Hex-encoded Ed25519 public key
body: rawBody,
signature: req.headers["x-signature-ed25519"]!,
headers: {
"x-signature-timestamp": req.headers["x-signature-timestamp"]!,
},
});Headers Required:
x-signature-ed25519: Hex-encoded Ed25519 signaturex-signature-timestamp: Unix timestamp
Note: Discord uses Ed25519 signatures, not HMAC. The secret should be the hex-encoded public key.
Unlimit
import { verifyUnlimit } from "webhook-verify-all";
const result = await verifyUnlimit({
secret: process.env.UNLIMIT_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["x-signature"]!,
});Headers Required:
x-signature: HMAC-SHA256 signature
GitHub
import { verifyGitHub } from "webhook-verify-all";
const result = await verifyGitHub({
secret: process.env.GITHUB_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["x-hub-signature-256"]!,
});Headers Required:
x-hub-signature-256: Formatsha256=signature
GitLab
import { verifyGitLab } from "webhook-verify-all";
const result = await verifyGitLab({
secret: process.env.GITLAB_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["x-gitlab-token"]!,
});Headers Required:
x-gitlab-token: Shared secret token (must match the secret you configured)
Note: GitLab uses simple token equality check (constant-time comparison), not HMAC. The token in the header must exactly match your configured webhook secret.
Twilio
import { verifyTwilio } from "webhook-verify-all";
const result = await verifyTwilio({
secret: process.env.TWILIO_AUTH_TOKEN!,
body: rawBody,
signature: req.headers["x-twilio-signature"]!,
});Headers Required:
x-twilio-signature: HMAC-SHA1 signature (base64 encoded)
Note: Twilio uses HMAC-SHA1, not SHA256.
SendGrid
import { verifySendGrid } from "webhook-verify-all";
const result = await verifySendGrid({
secret: process.env.SENDGRID_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["x-sendgrid-signature"]!,
});Headers Required:
x-sendgrid-signature: Signature header- Alternative:
x-twilio-email-event-webhook-signature(legacy)
Note: SendGrid Event Webhook uses Ed25519 signatures with X-Twilio-Email-Event-Webhook-Signature and ...-Timestamp headers (not HMAC). This implementation provides a simplified HMAC-based approach. For production use, consider implementing Ed25519 verification with SendGrid's public key.
Auth0
import { verifyAuth0 } from "webhook-verify-all";
const result = await verifyAuth0({
secret: process.env.AUTH0_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["auth0-signature"]!,
});Headers Required:
auth0-signature: HMAC-SHA256 signature
Replay Protection
Prevent replay attacks by using a replay store:
In-Memory Store
import { verifyWebhook, InMemoryReplayStore } from "webhook-verify-all";
const replayStore = new InMemoryReplayStore({ maxSize: 10000 });
const result = await verifyWebhook("stripe", {
secret: process.env.STRIPE_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["stripe-signature"]!,
replayStore,
});Redis Store
import Redis from "ioredis";
import { verifyWebhook, RedisReplayStore } from "webhook-verify-all";
const redis = new Redis(process.env.REDIS_URL);
const replayStore = new RedisReplayStore({ client: redis });
const result = await verifyWebhook("stripe", {
secret: process.env.STRIPE_WEBHOOK_SECRET!,
body: rawBody,
signature: req.headers["stripe-signature"]!,
replayStore,
});Configuration Options
VerificationOptions
interface VerificationOptions {
secret: string; // Secret key for verification
body: string | Buffer; // Raw request body (exact bytes)
signature: string; // Signature header value
timestamp?: string; // Optional timestamp header
timestampTolerance?: number; // Tolerance in seconds (default: 300)
clockSkew?: number; // Clock skew in seconds (default: 0)
replayStore?: ReplayStore; // Optional replay protection
}MiddlewareOptions
interface MiddlewareOptions {
provider: WebhookProvider; // Provider to verify
secret: string; // Secret key
timestampTolerance?: number; // Optional timestamp tolerance
clockSkew?: number; // Optional clock skew
replayStore?: ReplayStore; // Optional replay store
getRawBody?: (req: any) => string | Buffer; // Custom raw body extractor
}Error Handling
The package provides detailed error classes:
import {
WebhookVerificationError,
MissingHeaderError,
BadSignatureError,
TimestampError,
ReplayError,
} from "webhook-verify-all";
try {
const result = await verifyWebhook("stripe", options);
if (!result.ok) {
// Handle verification failure
console.error(result.errorCode, result.error);
}
} catch (error) {
if (error instanceof MissingHeaderError) {
// Missing required header
} else if (error instanceof BadSignatureError) {
// Signature verification failed
} else if (error instanceof TimestampError) {
// Timestamp validation failed
} else if (error instanceof ReplayError) {
// Replay attack detected
}
}Error Codes
ERR_NO_HEADER- Required header is missingERR_BAD_SIG- Signature verification failedERR_TIMESTAMP- Timestamp validation failed (expired or invalid)ERR_REPLAY- Replay attack detected (signature already used)ERR_UNSUPPORTED- Provider is not supportedERR_VERIFICATION_FAILED- General verification failure
Security Best Practices
✅ Do
- Always use the exact raw body bytes received from the webhook
- Store secrets securely (environment variables, secret managers)
- Enable replay protection in production
- Set appropriate timestamp tolerance (typically 5 minutes)
- Use constant-time comparison (handled automatically)
- Validate webhooks before processing business logic
- Log verification failures for monitoring
❌ Don't
- Don't parse the body as JSON before verification
- Don't modify the raw body in any way
- Don't use weak secrets or hardcode them
- Don't disable timestamp validation
- Don't ignore verification failures
- Don't use string comparison for signatures (use this library)
Raw Body Handling
Express
import express from "express";
const app = express();
// Parse raw body for webhook routes
app.use("/webhook", express.raw({ type: "application/json" }));
// Other routes can use JSON parser
app.use(express.json());Fastify
import Fastify from "fastify";
const fastify = Fastify();
// Use rawBody plugin or custom parser
fastify.addContentTypeParser(
"application/json",
{ parseAs: "buffer" },
(req, body, done) => {
done(null, body);
}
);TypeScript Support
Full TypeScript support with strict mode:
import type {
WebhookProvider,
VerificationResult,
VerificationOptions,
ReplayStore,
} from "webhook-verify-all";
const provider: WebhookProvider = "stripe";
const result: VerificationResult = await verifyWebhook(provider, options);Testing
Run tests with Vitest:
npm testWatch mode:
npm run test:watchArchitecture
- Provider Registry Pattern - No switch/if-else chains, uses a registry map
- Pluggable Replay Store - Interface-based design for extensibility
- Framework Agnostic Core - Core logic independent of Express/Fastify
- Type-Safe - Full TypeScript with strict mode
- Minimal Dependencies - Only ioredis for Redis support (optional)
Comparison with Other Packages
| Feature | webhook-verify-all | Others | |---------|-------------------|--------| | Multiple Providers | ✅ 12+ providers | ❌ Usually 1-2 providers | | TypeScript Support | ✅ Full strict mode | ⚠️ Partial or none | | Replay Protection | ✅ Built-in | ❌ Manual implementation | | Framework Support | ✅ Express & Fastify | ⚠️ Limited | | Zero Dependencies | ✅ Native crypto only | ⚠️ External crypto libs | | Unified API | ✅ Consistent interface | ❌ Different APIs per provider |
Contributing
Contributions are welcome! Please ensure:
- All tests pass
- TypeScript compiles without errors
- Code follows existing patterns
- New providers include tests
License
MIT
Security
If you discover a security vulnerability, please do not open a public issue. Instead, please email [email protected] with details.
Changelog
1.0.3
- Removed GitHub repository links (private repository)
- Updated package.json metadata (removed repository, bugs, homepage fields)
- Simplified Contributing section
1.0.2
- Added badges (npm version, downloads, license, TypeScript)
- Added "Why webhook-verify-all?" section
- Added comparison table with other packages
- Improved package description for better SEO
- Fixed repository URL format
1.0.1
- Fixed repository URL format
- Improved error handling for Discord Ed25519 verification
- Performance improvements (removed unnecessary async operations)
1.0.0
- Initial release
- Support for 12 providers: Stripe, Slack, Shopify, PayPal, Telegram, Discord, GitHub, GitLab, Twilio, SendGrid, Auth0, Unlimit
- In-memory and Redis replay protection
- Express and Fastify middleware
- Full TypeScript support
- 109+ unit and integration tests
