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
| Header | Description |
|---|---|
X-Webhook-Id | Unique delivery identifier — use for idempotency checks |
X-Webhook-Event | Event type (e.g., order.created) |
X-Webhook-Signature | HMAC-SHA256 signature |
X-Webhook-Timestamp | Unix timestamp of when the signature was generated |
Signature Algorithm
Whatalo generates the signature as follows:
- Concatenate:
${timestamp}.${rawBody} - Compute HMAC-SHA256 using your client secret as the key
- Hex-encode the result
- 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.timingSafeEqualor the SDK helper. String equality (===) is vulnerable to timing attacks. - Enforce the replay window — Reject requests with a
X-Webhook-Timestampolder than 5 minutes. - Validate
X-Webhook-Event— Only process event types your plugin declared in the manifest. - Store
X-Webhook-Idfor 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-storeThe 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.