Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.prudra.dev/llms.txt

Use this file to discover all available pages before exploring further.

Verify signatures

Every webhook delivery includes two headers you must verify:
  • X-Prudra-Signature — HMAC-SHA256 of the raw request body
  • X-Prudra-Timestamp — Unix timestamp of delivery
Signature verification confirms the webhook was sent by Prudra and the body was not tampered with. Timestamp verification prevents replay attacks.

Verification with the SDK

import express from 'express';
import { verifyWebhook } from '@prudra/webhooks';

app.post(
  '/webhooks/prudra',
  express.raw({ type: '*/*' }),  // must run BEFORE express.json()
  (req, res) => {
    res.sendStatus(200);  // always respond 200 quickly

    const isValid = verifyWebhook({
      payload:   req.body as Buffer,
      signature: req.headers['x-prudra-signature'] as string,
      timestamp: req.headers['x-prudra-timestamp'] as string,
      secret:    process.env.PRUDRA_WEBHOOK_SECRET!,
    });

    if (!isValid) {
      console.warn('Invalid webhook signature — ignoring');
      return;
    }

    const event = JSON.parse((req.body as Buffer).toString());
    // Handle event...
  }
);

Manual verification

If you’re not using the SDK:
import crypto from 'crypto';

function verifyWebhookSignature(
  rawBody:   Buffer,
  signature: string,
  timestamp: string,
  secret:    string,
): boolean {
  // Check timestamp is within 5 minutes to prevent replay
  const fiveMinutes = 5 * 60 * 1000;
  const ts = parseInt(timestamp, 10) * 1000;
  if (Math.abs(Date.now() - ts) > fiveMinutes) {
    return false;
  }

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

  const received = signature.replace('sha256=', '');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex'),
  );
}

Signature format

The X-Prudra-Signature header value is:
sha256=<hex-digest>
The signed payload is:
<timestamp>.<raw-body>
Where <timestamp> is the value of X-Prudra-Timestamp and <raw-body> is the raw bytes of the request body.

Critical: use raw body

express.json() parses and discards the raw bytes. You must use express.raw() on the webhook route before express.json():
// Webhook route — uses express.raw()
app.post('/webhooks/prudra', express.raw({ type: '*/*' }), handler);

// Other routes — uses express.json() normally
app.use(express.json());
If express.json() has already parsed the body, req.body is a JavaScript object, not a Buffer — signature verification will fail.