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
{
"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
| Event | Description |
|---|---|
payin.succeeded | Payment confirmed and wallet credited |
payin.failed | Payment failed at the channel |
payin.expired | Virtual account expired with no payment received |
Payout events
| Event | Description |
|---|---|
payout.succeeded | Funds delivered to beneficiary |
payout.failed | Payout rejected by channel |
payout.reversed | Payout succeeded but later reversed |
Conversion events
| Event | Description |
|---|---|
conversion.succeeded | Currency exchange completed |
conversion.failed | Exchange 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.
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
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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 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
200immediately and handle the event in a background job. - Be idempotent. You may receive the same event more than once. Check
idbefore 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.
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.