npm install ternimport { WebhookVerificationService } from 'tern';
// Verify a GitHub webhook
const result = await WebhookVerificationService.verifyWithPlatformConfig(
request,
'github',
'your-github-secret',
300
);
if (result.isValid) {
console.log('Webhook verified successfully:', result.payload);
} else {
console.error('Verification failed:', result.error);
}- GitHub:
x-hub-signature-256header - Stripe:
stripe-signatureheader - Shopify:
x-shopify-hmac-sha256header - Vercel:
x-vercel-signatureheader - Polar:
webhook-signatureheader (Standard Webhooks) - Dodo Payments:
webhook-signatureheader - Paddle:
paddle-signatureheader - Razorpay:
x-razorpay-signatureheader - Lemon Squeezy:
x-signatureheader - Auth0:
x-auth0-signatureheader - WorkOS:
workos-signatureheader - WooCommerce:
x-wc-webhook-signatureheader - ReplicateAI:
webhook-signatureheader - fal.ai:
x-fal-webhook-signatureheader (ED25519)
- Clerk: Custom base64 encoding
- Supabase: Token-based authentication
All built-in platforms work automatically with framework adapters (express, nextjs, cloudflare, hono) by setting platform and secret.
// Next.js - Paddle
import { createWebhookHandler } from '@hookflo/tern/nextjs';
export const POST = createWebhookHandler({
platform: 'paddle',
secret: process.env.PADDLE_WEBHOOK_SECRET!,
handler: async (payload) => ({ received: true, type: payload.event_type ?? payload.type }),
});// Express - Auth0
import { createWebhookMiddleware } from '@hookflo/tern/express';
app.post('/webhooks/auth0', createWebhookMiddleware({
platform: 'auth0',
secret: process.env.AUTH0_WEBHOOK_SECRET!,
}), (req, res) => res.json({ received: true }));// Cloudflare - WooCommerce
import { createWebhookHandler } from '@hookflo/tern/cloudflare';
const handleWoo = createWebhookHandler({
platform: 'woocommerce',
secretEnv: 'WOO_WEBHOOK_SECRET',
handler: async () => ({ received: true }),
});// Hono - Stripe
import { Hono } from 'hono';
import { createWebhookHandler } from '@hookflo/tern/hono';
const app = new Hono();
app.post('/webhooks/stripe', createWebhookHandler({
platform: 'stripe',
secret: process.env.STRIPE_WEBHOOK_SECRET!,
handler: async (payload, metadata, c) => c.json({
received: true,
id: metadata.id,
payload,
}),
}));fal.ai verification is ED25519-based and requires x-fal-webhook-signature plus fal request metadata headers.
For framework adapters, pass a PEM public key via secret:
import { createWebhookHandler } from '@hookflo/tern/nextjs';
export const POST = createWebhookHandler({
platform: 'falai',
secret: process.env.FAL_PUBLIC_KEY_PEM!,
handler: async (payload, metadata) => ({ received: true, requestId: metadata.requestId }),
});Verify a webhook using platform-specific configuration.
const result = await WebhookVerificationService.verifyWithPlatformConfig(
request,
'github',
'your-secret',
300 // optional, default: 300
);Verify a webhook using simple token-based authentication (like Supabase).
const result = await WebhookVerificationService.verifyTokenBased(
request,
'your-webhook-id',
'your-webhook-token'
);Verify a webhook using a custom configuration object.
const config = {
platform: 'custom',
secret: 'your-secret',
signatureConfig: {
algorithm: 'hmac-sha256',
headerName: 'x-signature',
headerFormat: 'prefixed',
prefix: 'sha256=',
payloadFormat: 'raw'
}
};
const result = await WebhookVerificationService.verify(request, config);Automatically detect the platform from request headers.
import { detectPlatformFromHeaders } from 'tern';
const platform = detectPlatformFromHeaders(request.headers);
if (platform) {
const result = await WebhookVerificationService.verifyWithPlatformConfig(
request, platform, 'your-secret', 300
);
}import express from 'express';
import { WebhookVerificationService } from 'tern';
const app = express();
app.post('/webhook', async (req, res) => {
try {
const result = await WebhookVerificationService.verifyWithPlatformConfig(
req,
'github',
process.env.GITHUB_WEBHOOK_SECRET,
300
);
if (result.isValid) {
console.log('Webhook received:', result.payload);
res.status(200).json({ success: true });
} else {
res.status(401).json({ error: result.error });
}
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});// pages/api/webhook.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { WebhookVerificationService } from 'tern';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const result = await WebhookVerificationService.verifyWithPlatformConfig(
req as any,
'stripe',
process.env.STRIPE_WEBHOOK_SECRET,
300
);
if (result.isValid) {
console.log('Stripe webhook:', result.payload);
res.status(200).json({ received: true });
} else {
res.status(401).json({ error: result.error });
}
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
}import { detectPlatformFromHeaders, WebhookVerificationService } from 'tern';
async function handleWebhook(request: Request) {
const platform = detectPlatformFromHeaders(request.headers);
if (!platform) {
throw new Error('Unknown webhook platform');
}
const secret = getSecretForPlatform(platform); // Your secret management
const result = await WebhookVerificationService.verifyWithPlatformConfig(
request,
platform,
secret,
300
);
return result;
}import { WebhookVerificationService } from 'tern';
// For platforms that use simple token-based auth
const result = await WebhookVerificationService.verifyTokenBased(
request,
'your-webhook-id',
'your-webhook-token'
);
if (result.isValid) {
console.log('Token-based webhook verified:', result.payload);
}try {
const result = await WebhookVerificationService.verifyWithPlatformConfig(
request,
'github',
'your-secret',
300
);
if (!result.isValid) {
console.error('Verification failed:', result.error);
return res.status(401).json({ error: result.error });
}
// Process webhook
console.log('Webhook verified:', result.payload);
} catch (error) {
console.error('Verification error:', error);
return res.status(500).json({ error: 'Internal server error' });
}import { WebhookVerificationService } from 'tern';
// Create a mock request
const mockRequest = new Request('http://localhost/webhook', {
method: 'POST',
headers: {
'x-hub-signature-256': 'sha256=abc123',
'content-type': 'application/json'
},
body: JSON.stringify({ test: 'data' })
});
const result = await WebhookVerificationService.verifyWithPlatformConfig(
mockRequest,
'github',
'test-secret',
300
);
console.log('Test result:', result);const customConfig = {
platform: 'custom',
secret: 'your-custom-secret',
signatureConfig: {
algorithm: 'hmac-sha256',
headerName: 'x-custom-signature',
headerFormat: 'raw',
payloadFormat: 'raw'
}
};
const result = await WebhookVerificationService.verify(request, customConfig);const customConfig = {
platform: 'custom',
secret: 'your-secret',
signatureConfig: {
algorithm: 'custom',
headerName: 'x-custom-signature',
headerFormat: 'raw',
payloadFormat: 'raw',
customConfig: {
type: 'your-custom-type',
// Add your custom configuration here
}
}
};All verification methods return a WebhookVerificationResult object:
interface WebhookVerificationResult {
isValid: boolean;
error?: string;
platform: WebhookPlatform;
payload?: any;
metadata?: {
timestamp?: string;
id?: string;
[key: string]: any;
};
}// Success
{
isValid: true,
platform: 'github',
payload: { event: 'push', repository: { name: 'test' } },
metadata: {
timestamp: '1234567890',
algorithm: 'hmac-sha256'
}
}
// Error
{
isValid: false,
error: 'Invalid signature',
platform: 'github'
}- Timing-safe comparisons: Prevents timing attacks
- Proper validation: Comprehensive input validation
- Secure defaults: Secure by default configuration
- Algorithm flexibility: Support for multiple signature algorithms
- Zero dependencies: Only uses Node.js built-in modules
- Optimized algorithms: Efficient HMAC verification
- Minimal overhead: Lightweight and fast
- TypeScript support: Full type safety and IntelliSense