Skip to content

docs / webhooks

Webhooks

Payve sends POST requests to your registered endpoints whenever a significant event occurs. This is the recommended way to keep your system in sync with payment state changes.

Registering an endpoint

Add a webhook endpoint from Dashboard → Developer → Webhooks. You can subscribe to specific events or use * to receive all events.

Webhook event object

json
{
  "id": "evt_01j9abc999",
  "type": "payin.succeeded",
  "merchantId": "mer_01j9abc000",
  "data": {
    "publicId": "pin_01j9abc123",
    "status": "succeeded",
    "currency": "NGN",
    "amount": "500000",
    "merchantReference": "ORDER-1234"
  },
  "createdAt": "2025-01-15T10: 02: 30Z"
}

Available events

Payin events

EventDescription
payin.succeededPayment confirmed and wallet credited
payin.failedPayment failed at the channel
payin.expiredVirtual account expired with no payment received

Payout events

EventDescription
payout.succeededFunds delivered to beneficiary
payout.failedPayout rejected by channel
payout.reversedPayout succeeded but later reversed

Conversion events

EventDescription
conversion.succeededCurrency exchange completed
conversion.failedExchange could not be executed

Verifying signatures

Every webhook request includes an X-Payve-Signature header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.

Always verify this signature before processing a webhook.

ts
import crypto from 'crypto';

function verifyWebhook(
  rawBody: Buffer,
  signature: string,
  secret: string,
): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

// In your Express handler:
app.post('/webhooks/payve', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-payve-signature'] as string;
  if (!verifyWebhook(req.body, sig, process.env.PAYVE_WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body.toString());
  // handle event...
  res.status(200).send('OK');
});

🚨

Do not skip signature verification

Always verify the signature before acting on a webhook payload. Failing to do so exposes your system to spoofed events.


Responding to webhooks

Your endpoint must return a 2xx response within 10 seconds. If it times out or returns a non-2xx status, Payve retries with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours

After 6 failed attempts the event is marked as dead. You can replay dead events from the dashboard.


Best practices

  • Acknowledge fast, process async. Return 200 immediately and handle the event in a background job.
  • Be idempotent. You may receive the same event more than once. Check id before processing.
  • Don't trust the event body alone. After receiving payin.succeeded, fetch the payin via the API to confirm its status before fulfilling an order.
ts
app.post('/webhooks/payve', async (req, res) => {
  // 1. Verify signature
  // 2. Acknowledge immediately
  res.status(200).send('OK');

  // 3. Process asynchronously
  const event = JSON.parse(req.body.toString());
  if (event.type === 'payin.succeeded') {
    // Fetch and confirm state from API
    const payin = await payveClient.payins.get(event.data.publicId);
    if (payin.status === 'succeeded') {
      await fulfillOrder(payin.merchantReference);
    }
  }
});

Test webhooks

Use the dashboard's Send test event button to fire a sample payload to your endpoint. In sandbox mode, the Payve simulator also triggers real webhook events.