Skip to main content

Verifying Webhooks

Every Payvessel webhook is signed with your secret key and sent from a known IP address. Always verify both signals—and prevent duplicate processing—before performing irreversible business actions.

Signature Verification

  1. Read the raw request body exactly as received.
  2. Compute an HMAC using SHA-512 with your secret (PVSECRET-) as the key.
  3. Compare the result with the HTTP_PAYVESSEL_HTTP_SIGNATURE header using a constant-time comparison.
Parsing JSON before calculating the signature can change whitespace and break verification. Always hash the raw payload first.

IP Allowlist

Accept webhook requests only from the following Payvessel IP addresses:
  • 3.255.23.38
  • 162.246.254.36
When hosting behind a proxy or load balancer, read the left-most entry from the X-Forwarded-For header; otherwise, fall back to the connection’s remote address.

Duplicate Prevention

  • Store processed transaction.reference (or trackingReference) values in persistent storage.
  • Wrap webhook logic in idempotent database transactions.
  • Return 200 OK only after your state changes succeed; otherwise Payvessel will retry.

End-to-End Examples

The following implementations verify signature, validate IP addresses, guard against duplicates, and respond with appropriate status codes.
import crypto from 'crypto';
import express from 'express';

const app = express();
const SECRET = process.env.PAYVESSEL_SECRET || 'PVSECRET-';
const TRUSTED_IPS = ['3.255.23.38', '162.246.254.36'];

app.post('/webhooks/payvessel', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.header('HTTP_PAYVESSEL_HTTP_SIGNATURE');
  const payload = req.body; // Buffer
  const hash = crypto.createHmac('sha512', SECRET).update(payload).digest('hex');

  // Determine caller IP (supports proxies)
  const ip =
    req.headers['x-forwarded-for']?.toString().split(',')[0].trim() ??
    req.socket.remoteAddress;

  if (signature !== hash || !TRUSTED_IPS.includes(ip)) {
    return res.status(400).json({ message: 'Invalid signature or IP' });
  }

  const data = JSON.parse(payload.toString());
  const reference = data.transaction.reference;

  if (await hasProcessed(reference)) {
    return res.status(200).json({ message: 'Already processed' });
  }

  await markProcessed(reference);
  await handleBusinessLogic(data);

  return res.status(200).json({ message: 'success' });
});

Failure Handling

  • Respond with 4xx for security violations (invalid signature, unknown IP).
  • Respond with 5xx when internal processing fails so Payvessel retries automatically.
  • Implement alerting for repeated failures and monitor retry logs.