PlatformXeDocs
Get API Key

Signature Verification

Verify webhook payload authenticity using HMAC-SHA256 signatures and prevent replay attacks.

Every webhook payload from PlatformXe includes an HMAC-SHA256 signature. Verify this signature to confirm the payload was sent by PlatformXe and has not been tampered with.

Signature headers

Each webhook delivery includes these headers:

HeaderDescription
X-Event-SignatureHMAC-SHA256 signature of the raw request body
X-Event-TimestampUnix timestamp (seconds) when the payload was signed
X-Event-TypeThe event type (e.g. email.sent)
X-Event-IdUnique delivery ID for deduplication

How signatures work

PlatformXe computes the signature by:

  1. Concatenating the timestamp and raw JSON body with a . separator: {timestamp}.{body}
  2. Computing an HMAC-SHA256 hash of that string using your webhook secret
  3. Encoding the result as a hex string
signature = HMAC-SHA256(secret, "{timestamp}.{body}")

Verification example (Node.js)

import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhookSignature(
  rawBody: string,
  signature: string,
  timestamp: string,
  secret: string
): boolean {
  // 1. Check timestamp to prevent replay attacks (5-minute tolerance)
  const currentTime = Math.floor(Date.now() / 1000);
  const signedTime = parseInt(timestamp, 10);
  if (Math.abs(currentTime - signedTime) > 300) {
    return false; // Timestamp too old or too far in the future
  }

  // 2. Compute expected signature
  const payload = `${timestamp}.${rawBody}`;
  const expected = createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // 3. Compare using timing-safe comparison
  const expectedBuffer = Buffer.from(expected, 'utf-8');
  const receivedBuffer = Buffer.from(signature, 'utf-8');

  if (expectedBuffer.length !== receivedBuffer.length) {
    return false;
  }

  return timingSafeEqual(expectedBuffer, receivedBuffer);
}

Usage in an Express handler

import express from 'express';

const app = express();

app.post(
  '/webhooks/platformxe',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-event-signature'] as string;
    const timestamp = req.headers['x-event-timestamp'] as string;
    const eventType = req.headers['x-event-type'] as string;
    const eventId = req.headers['x-event-id'] as string;

    const rawBody = req.body.toString('utf-8');

    const isValid = verifyWebhookSignature(
      rawBody,
      signature,
      timestamp,
      process.env.WEBHOOK_SECRET!
    );

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

    const payload = JSON.parse(rawBody);

    // Process the event
    console.log(`Received ${eventType} (${eventId}):`, payload);

    res.status(200).json({ received: true });
  }
);

Replay protection

The X-Event-Timestamp header enables replay attack prevention. Compare the signed timestamp against the current time and reject payloads that are too old.

A 5-minute tolerance window (300 seconds) is recommended. This accounts for network delays while still rejecting replayed payloads.

Deduplication

Use the X-Event-Id header to deduplicate deliveries. Store processed event IDs and skip any payload with an ID you have already handled. This protects against duplicate deliveries caused by retries.

Always use timingSafeEqual (or equivalent) for signature comparison. Standard string comparison (===) is vulnerable to timing attacks.