Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs-alpha.pepay.io/llms.txt

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

Overview

Pepay signs every webhook request so you can verify authenticity and integrity. Validation requires the raw request body plus the X-Pepay-Timestamp and signature headers.

Authentication

  • X-Pepay-Timestamp
  • X-Pepay-Signature
  • X-Pepay-Signature-Previous (optional during rotation)

Request

Signed payload format

  • ${timestamp_ms}.${raw_body}
  • Signature: HMAC_SHA256(secret, signed_payload) (hex)
Important:
  • Verify against the raw request body bytes, not a re-serialized JSON object.
  • Reject requests with timestamps outside a small tolerance window (recommended: 5 minutes).

Response

For successful verification, return a fast 2xx acknowledgment:
{
  "ok": true
}

Errors

  • 400 invalid_signature when signature comparison fails.
  • 400 timestamp_out_of_range when the timestamp is outside your tolerance window.
  • Any non-2xx status can trigger retries from Pepay webhook delivery.

Examples

Node (no SDK)

const crypto = require('crypto');
const express = require('express');

function timingSafeEqualHex(left, right) {
  const a = Buffer.from(String(left || ''), 'hex');
  const b = Buffer.from(String(right || ''), 'hex');
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

function computeSignature(secret, timestampMs, rawBody) {
  const signed = `${timestampMs}.${rawBody}`;
  return crypto.createHmac('sha256', secret).update(signed).digest('hex');
}

const app = express();
app.post('/webhooks/pepay', express.raw({ type: 'application/json' }), (req, res) => {
  const timestampMs = req.get('X-Pepay-Timestamp');
  const signature = req.get('X-Pepay-Signature');
  const signaturePrevious = req.get('X-Pepay-Signature-Previous');
  const rawBody = req.body.toString('utf8');

  const nowMs = Date.now();
  if (Math.abs(nowMs - Number(timestampMs)) > 5 * 60 * 1000) {
    return res.status(400).send('timestamp_out_of_range');
  }

  const expected = computeSignature(process.env.PEPAY_WEBHOOK_SECRET, timestampMs, rawBody);
  const expectedPrevious = process.env.PEPAY_WEBHOOK_SECRET_PREVIOUS
    ? computeSignature(process.env.PEPAY_WEBHOOK_SECRET_PREVIOUS, timestampMs, rawBody)
    : null;

  const ok =
    timingSafeEqualHex(signature, expected) ||
    (expectedPrevious && timingSafeEqualHex(signaturePrevious, expectedPrevious));

  if (!ok) return res.status(400).send('invalid_signature');
  return res.status(200).send('ok');
});

SDK helper (Node)

import express from 'express';
import { createWebhookVerifier } from 'pepay';

const verifier = createWebhookVerifier({
  secrets: [process.env.PEPAY_WEBHOOK_SECRET, process.env.PEPAY_WEBHOOK_SECRET_PREVIOUS].filter(Boolean),
  toleranceMs: 5 * 60 * 1000
});

app.post('/webhooks/pepay', express.raw({ type: 'application/json' }), async (req, res) => {
  const result = await verifier.verify({ rawBody: req.body, headers: req.headers });
  if (!result.valid) return res.status(400).send(result.reason || 'invalid_signature');
  return res.status(200).send('ok');
});
See Webhooks (merchant) for the full SDK walkthrough.

Multiple endpoints and rotation

  • If you run multiple endpoints, keep a list of active secrets and verify against any.
  • Use X-Pepay-Network-Environment to route devnet vs mainnet endpoints if you want strict separation.
  • During rotation, keep the previous secret configured until all endpoints are updated.

Failure behavior

  • Return 2xx quickly after enqueueing work.
  • Non-2xx responses are retried with backoff, so keep handlers idempotent.
Next: WebSockets