> ## 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.

# Webhook Authentication

> Signature verification for webhook deliveries.

## 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:

```json theme={null}
{
  "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)

```js theme={null}
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)

```ts theme={null}
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)](/sdk/merchants/webhooks) 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](/api-spec/realtime/websockets)
