Webhook Verifier
The @commercejs/webhook-verifier package provides cryptographic webhook verification. It ships with a built-in Tap configuration and supports custom providers through an extensible config system.
Installation
pnpm add @commercejs/webhook-verifier
Quick Start
import { WebhookVerifier } from '@commercejs/webhook-verifier'
import { tap as tapConfig } from '@commercejs/webhook-verifier/configs'
const verifier = new WebhookVerifier({
...tapConfig,
secretKey: process.env.TAP_SECRET_KEY!,
})
const result = verifier.verify(webhookBody, requestHeaders)
if (result.isValid) {
console.log('Webhook verified')
} else {
console.error('Verification failed:', result.error)
}
How It Works
Each webhook provider has a different verification strategy. The WebhookVerifier abstracts this through a configuration object:
- Formatter — Extracts and concatenates fields from the webhook body into a hashstring
- Algorithm — Applies a cryptographic hash (HMAC-SHA256, SHA-256, etc.)
- getExpectedSignature — Retrieves the signature from the request headers for comparison
Tap Configuration
Tap uses a hashstring verification pattern. The verifier concatenates specific fields from the charge body, hashes them with your secret key, and compares against a header signature:
import { tap } from '@commercejs/webhook-verifier/configs'
// The tap config defines:
// - formatter: extracts id, amount, currency, gateway.reference, created
// - algorithm: HMAC-SHA256
// - getExpectedSignature: reads 'hashstring' header
Hashstring Format
Tap constructs the hashstring by concatenating these fields:
x_id + x_amount + x_currency + x_gateway_reference + x_created + secret_key
The verifier replicates this format, hashes the result, and compares it against the hashstring header sent by Tap.
Custom Provider Configuration
Create a configuration for any webhook provider:
import { WebhookVerifier } from '@commercejs/webhook-verifier'
const stripeConfig = {
formatter: (body: any) => {
// Extract fields in Stripe's expected order
return `${body.id}.${body.created}`
},
algorithm: 'hmac-sha256' as const,
getExpectedSignature: (headers: Record<string, string>) => {
return headers['stripe-signature'] ?? null
},
}
const verifier = new WebhookVerifier({
...stripeConfig,
secretKey: process.env.STRIPE_WEBHOOK_SECRET!,
})
API
WebhookVerifier
class WebhookVerifier {
constructor(config: WebhookVerifierConfig)
verify(body: unknown, headers: Record<string, string>): VerificationResult
}
WebhookVerifierConfig
| Property | Type | Description |
|---|---|---|
secretKey | string | Secret key for HMAC signing |
formatter | (body: any) => string | Converts webhook body to hashstring |
algorithm | 'hmac-sha256' | 'sha-256' | Hash algorithm |
getExpectedSignature | (headers) => string | null | Extracts expected signature from headers |
VerificationResult
interface VerificationResult {
isValid: boolean
error?: string
}
Testing with ngrok
For local webhook testing, use ngrok to expose your local server:
ngrok http 3100
Update your APP_URL environment variable with the ngrok URL so that webhook URLs in charge requests point to your tunnel.