Macropay uses the Standard Webhooks specification to sign all webhook deliveries. You should verify the signature of every webhook you receive to ensure it was sent by Macropay and has not been tampered with.

How It Works

Each webhook delivery includes three headers:
HeaderDescription
webhook-idA unique identifier for this webhook event
webhook-timestampUnix timestamp (seconds) when the event was sent
webhook-signatureOne or more Base64-encoded HMAC-SHA256 signatures, comma-separated
The signature is computed over the string {webhook-id}.{webhook-timestamp}.{body} using your endpoint’s signing secret as the HMAC key.

Getting Your Signing Secret

Your webhook signing secret is generated when you create a webhook endpoint. You can find it in:
  1. The Macropay dashboard under Settings > Webhooks > select your endpoint
  2. The API response when creating a webhook endpoint
The secret starts with whsec_ followed by a Base64-encoded key.

Verification Examples

The easiest approach is to use the official Standard Webhooks libraries, which handle all the details for you.
import { Webhook } from "standardwebhooks";

const WEBHOOK_SECRET = process.env.MACROPAY_WEBHOOK_SECRET!;

export async function handleWebhook(request: Request): Promise<Response> {
  const body = await request.text();

  const headers = {
    "webhook-id": request.headers.get("webhook-id")!,
    "webhook-timestamp": request.headers.get("webhook-timestamp")!,
    "webhook-signature": request.headers.get("webhook-signature")!,
  };

  const wh = new Webhook(WEBHOOK_SECRET);

  try {
    const payload = wh.verify(body, headers);
    // payload is the verified and parsed webhook event
    console.log("Verified event:", payload.type);

    // Handle the event...
    return new Response("OK", { status: 200 });
  } catch (err) {
    console.error("Webhook verification failed:", err);
    return new Response("Invalid signature", { status: 401 });
  }
}
Install the libraries:
npm install standardwebhooks

Manual Verification

If you prefer to verify signatures manually without a library:
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(
  body: string,
  headers: Record<string, string>,
  secret: string
): boolean {
  const msgId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];

  // 1. Check timestamp tolerance (5 minutes)
  const now = Math.floor(Date.now() / 1000);
  const ts = parseInt(timestamp, 10);
  if (Math.abs(now - ts) > 300) {
    throw new Error("Webhook timestamp too old or too new");
  }

  // 2. Compute expected signature
  const signedContent = `${msgId}.${timestamp}.${body}`;

  // Remove the "whsec_" prefix and decode the Base64 secret
  const secretBytes = Buffer.from(secret.replace("whsec_", ""), "base64");

  const expectedSignature = createHmac("sha256", secretBytes)
    .update(signedContent)
    .digest("base64");

  // 3. Compare against provided signatures (there may be multiple)
  const providedSignatures = signatures.split(" ");
  for (const sig of providedSignatures) {
    // Each signature is prefixed with version, e.g. "v1,<base64>"
    const [, sigValue] = sig.split(",");
    if (
      timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(sigValue)
      )
    ) {
      return true;
    }
  }

  return false;
}

Security Best Practices

Always verify signatures in production. Without verification, an attacker could send fake webhook events to your endpoint and trigger unauthorized actions in your system.
  1. Use timing-safe comparison: Always use timingSafeEqual (Node.js) or hmac.compare_digest (Python) to compare signatures. Regular string comparison is vulnerable to timing attacks.
  2. Check the timestamp: Reject events with timestamps older than 5 minutes to prevent replay attacks.
  3. Use HTTPS: Always configure your webhook endpoint with an HTTPS URL.
  4. Respond quickly: Return a 2xx status code within 30 seconds. Process the event asynchronously if needed.
  5. Handle retries: Macropay retries failed deliveries with exponential backoff. Make your handler idempotent to safely handle duplicate events. See Webhook Delivery for retry details.

Framework-Specific Helpers

If you are using one of our framework adapters, webhook verification is built in:

Next.js

Built-in webhook handler with automatic verification

Express

Middleware for Express webhook routes

Fastify

Plugin for Fastify webhook routes

Hono

Middleware for Hono webhook routes