Packages

Webhook Verifier

Provider-agnostic webhook verification with built-in presets for Tap and extensible configuration.

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:

  1. Formatter — Extracts and concatenates fields from the webhook body into a hashstring
  2. Algorithm — Applies a cryptographic hash (HMAC-SHA256, SHA-256, etc.)
  3. 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

PropertyTypeDescription
secretKeystringSecret key for HMAC signing
formatter(body: any) => stringConverts webhook body to hashstring
algorithm'hmac-sha256' | 'sha-256'Hash algorithm
getExpectedSignature(headers) => string | nullExtracts 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.

See the Webhook Setup guide for a complete local testing walkthrough.