Skip to Content
VantraAPI Docs
API reference

JavaScript callback helpers

Preview, copy, and understand every helper function.

Downloading is optional: preview and copy the complete code below. Download the file.

Use this module on your callback server. Copy it into verify-webhook.mjs or src/lib/verify-webhook.mjs in Next.js. Read the raw body before JSON middleware and use the webhook signing secret, which is different from the API secret.

Function reference

verifyWebhook

verifyWebhook(rawBody, headers, { secret, environment = undefined, now = Date.now() })

rawBody must be a Node.js Buffer of the original request bytes. headers is a Web Headers instance. environment is optional; omit it to verify with the webhook signing secret alone and read the signed event.environment. If supplied, TEST or LIVE restricts accepted events. Both payload environment fields must agree and contain TEST or LIVE. Match the verified environment to the saved payment before fulfillment. now is milliseconds and exists for deterministic tests. Returns the parsed event after signature, five-minute timestamp tolerance, environment, and envelope/header consistency checks. Throws on invalid input, signature, timestamp, JSON, or event. It does not persist, deduplicate, verify your order amount, or fulfill an order.

receiveWebhook

await receiveWebhook(rawBody, headers, { secret, environment, now, persistEvent })

Calls verifyWebhook, then awaits your persistEvent(event) adapter. Returns { status: 204 } only after the adapter resolves. Verification and persistence errors propagate to your HTTP handler. The adapter must atomically store a unique event ID and enqueue processing in durable storage; duplicates should resolve successfully. The returned object does not itself send an HTTP response. See the callback guide for handlers and order reconciliation.

Complete source code

verify-webhook.mjs
import { createHmac, timingSafeEqual } from 'node:crypto';

// Call BEFORE JSON middleware changes the bytes. Headers are a Web Headers instance.
// A 5-minute receiver tolerance is a recommended policy, not a gateway delivery guarantee.
export function verifyWebhook(rawBody, headers, { secret, environment, now = Date.now() }) {
  if (!secret || (environment !== undefined && !['TEST', 'LIVE'].includes(environment))) throw new Error('Webhook configuration missing');
  if (!Buffer.isBuffer(rawBody)) throw new Error('Raw Buffer required');
  const timestamp = headers.get('x-webhook-timestamp') ?? '';
  const signature = headers.get('x-webhook-signature') ?? '';
  if (!/^\d+$/.test(timestamp) || !/^[a-f0-9]{64}$/.test(signature)) throw new Error('Invalid webhook headers');
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(Math.floor(now / 1000) - seconds) > 300) throw new Error('Stale webhook');
  const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
  if (!timingSafeEqual(expected, Buffer.from(signature, 'hex'))) throw new Error('Invalid webhook signature');
  const event = JSON.parse(rawBody.toString('utf8'));
  if (typeof event.id !== 'string' || !event.id || event.id !== headers.get('x-webhook-id') ||
      !['TEST', 'LIVE'].includes(event.environment) || event.data?.environment !== event.environment ||
      (environment !== undefined && event.environment !== environment) ||
      typeof event.transactionId !== 'string' || event.transactionId !== event.data?.transactionId ||
      event.event !== headers.get('x-webhook-event') || event.event !== `PAYMENT_${event.data?.status}`) {
    throw new Error('Invalid webhook event');
  }
  return event;
}

// persistEvent must durably insert event.id with a UNIQUE constraint and enqueue work
// in one transaction. A duplicate is success. Throw on storage failure to request retry.
export async function receiveWebhook(rawBody, headers, options) {
  const event = verifyWebhook(rawBody, headers, options);
  await options.persistEvent(event);
  return { status: 204 };
}

Payment examples · Callback examples

On this page