OuiPay
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

HeaderExample valuePurpose
X-OuiPay-Signaturesha256=a1b2c3d4e5f6...HMAC hex digest
X-OuiPay-Timestamp1758158400Unix 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.

On this page