Webhooks
Webhook Verification
Verify webhook signatures before processing any event. HMAC-SHA256, timestamp checks, and implementation examples.
Every webhook delivery is HMAC-signed with your subscription's signing secret. Never process an unverified payload: signature verification is not optional for a financial integration.
How verification works
flowchart TD
A["Receive POST /webhooks"] --> B{"Verify HMAC signature"}
B -- Invalid --> C["Return 401, discard"]
B -- Valid --> D{"Timestamp < 5 min old?"}
D -- No --> E["Return 401, reject replay"]
D -- Yes --> F{"event_key processed?"}
F -- Yes --> G["Return 200, skip duplicate"]
F -- No --> H["Queue for processing"]
H --> I["Return 200"]Signature headers
| Header | Example value | Purpose |
|---|---|---|
X-OuiPay-Signature | sha256=a1b2c3d4e5f6... | HMAC hex digest |
X-OuiPay-Timestamp | 1758158400 | Unix seconds |
Computing the expected signature
HMAC_SHA256(signing_secret, timestamp + "." + raw_body)Compare the hex digest with the sha256= value from the X-OuiPay-Signature
header using a constant-time comparison.
Implementation
import crypto from "crypto";
function verifyWebhook(
rawBody: string,
signature: string,
timestamp: string,
secret: string,
): boolean {
// Reject timestamps older than 5 minutes (replay protection).
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`),
Buffer.from(signature),
);
}
// Express example
app.post("/webhooks/ouipay", (req, res) => {
const isValid = verifyWebhook(
req.rawBody,
req.headers["x-ouipay-signature"],
req.headers["x-ouipay-timestamp"],
process.env.OUIPAY_WEBHOOK_SECRET,
);
if (!isValid) return res.status(401).end();
const { event_key, event_type, data } = req.body;
// Dedupe: skip already-processed events.
if (alreadyProcessed(event_key)) return res.status(200).end();
// Ack fast, process async.
queue.push({ event_key, event_type, data });
res.status(200).end();
});Verify the raw body
HMAC covers the raw bytes. Verify before JSON parsing. If your framework parses the body first, ensure you also capture the raw bytes.