Webhooks

Verification & Security

Verify Whatalo webhook signatures using HMAC-SHA256 to prevent forged requests and replay attacks.

Every webhook request from Whatalo includes an HMAC-SHA256 signature. Always verify this signature before processing any event payload — it guarantees the request originated from Whatalo and has not been tampered with.

SDK Verification (Simple)

For most plugins, the SDK helper is the fastest path:

import { verifyWebhook } from "@whatalo/plugin-sdk/webhooks";

// rawBody must be the raw, unparsed request body string
const isValid = verifyWebhook({
  payload: rawBody,
  signature: req.headers["x-webhook-signature"] as string,
  timestamp: req.headers["x-webhook-timestamp"] as string,
  secret: process.env.WHATALO_CLIENT_SECRET!,
});

if (!isValid) {
  return res.status(401).json({ error: "Invalid signature" });
}

This performs HMAC-SHA256 verification using your client secret as the key and enforces the default 300-second replay protection window.

Manual Verification

Use the SDK helper when possible. If you need a custom verifier, keep the same checks:

import crypto from "node:crypto";

function verifyWhataloWebhook(
  headers: Record<string, string | string[] | undefined>,
  rawBody: string,
  secret: string
): boolean {
  const timestamp = getHeader(headers, "x-webhook-timestamp");
  const signature = getHeader(headers, "x-webhook-signature");

  if (!timestamp || !signature || !/^[a-f0-9]{64}$/i.test(signature)) return false;

  const timestampValue = Number(timestamp);
  if (!Number.isInteger(timestampValue)) return false;

  const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - timestampValue);
  if (ageSeconds > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature, "hex"),
      Buffer.from(expected, "hex")
    );
  } catch {
    return false;
  }
}

function getHeader(
  headers: Record<string, string | string[] | undefined>,
  name: string
): string {
  const value = headers[name];
  return Array.isArray(value) ? value[0] ?? "" : value ?? "";
}

Signature Headers

HeaderDescription
X-Webhook-IdUnique delivery identifier — use for idempotency checks
X-Webhook-EventEvent type (e.g., order.created)
X-Webhook-SignatureHMAC-SHA256 signature
X-Webhook-TimestampUnix timestamp of when the signature was generated

Signature Algorithm

Whatalo generates the signature as follows:

  1. Concatenate: ${timestamp}.${rawBody}
  2. Compute HMAC-SHA256 using your client secret as the key
  3. Hex-encode the result
  4. Send as X-Webhook-Signature

You can replicate this in any language:

import crypto from "node:crypto";

function computeSignature(rawBody: string, timestamp: string, secret: string): string {
  const signedContent = `${timestamp}.${rawBody}`;
  return crypto
    .createHmac("sha256", secret)
    .update(signedContent)
    .digest("hex");
}

Security Checklist

Skipping any of these checks opens your plugin to forged or replayed requests.

  • Use the raw body — Parse JSON only after verification. Any modification to the body before computing the HMAC will cause verification to fail.
  • Compare signatures in constant time — Use crypto.timingSafeEqual or the SDK helper. String equality (===) is vulnerable to timing attacks.
  • Enforce the replay window — Reject requests with a X-Webhook-Timestamp older than 5 minutes.
  • Validate X-Webhook-Event — Only process event types your plugin declared in the manifest.
  • Store X-Webhook-Id for idempotency — Webhook deliveries can be retried. Process each delivery ID only once.

Testing Webhooks Locally

Use the CLI to trigger test events against your local development server:

# Trigger an order.created event against your dev store
whatalo webhook trigger ORDER_CREATED --store my-dev-store

The CLI signs the request with your development client secret, so your verification logic runs exactly as it would in production. Only works with development stores.

See the CLI Reference — webhook for the full list of supported test events.

On this page