#
Webhooks
Receive real-time notifications when payment statuses change.
#
Overview
HivePay sends webhook notifications to your server when payment events occur. This allows your application to react immediately without polling the API.
#
Configuration
Configure your webhook URL in the Dashboard or via API:
await hivepay.merchants.update('merchant_id', {
webhookUrl: 'https://yoursite.com/webhooks/hivepay'
});
curl -X PUT https://hivepay.me/api/public/merchants/your_merchant_id \
-H "Authorization: Bearer sk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://yoursite.com/webhooks/hivepay"
}'
#
Webhook Format
#
Headers
#
Payload
{
"type": "payment.status_changed",
"data": {
"id": "cmj7b2rg10004d2rimvum8kaz",
"merchantId": "cmkfgjmim00008rcxp81upef9",
"status": "completed",
"providerId": "hive",
"internalId": "trx_abc123"
}
}
#
Event Types
#
Payment Statuses
#
Signature Verification
Always verify webhook signatures to ensure authenticity:
import { HivePay } from '@hivepay/client';
const hivepay = new HivePay({ apiKey: 'sk_live_xxx' });
async function handleWebhook(req: Request) {
const result = await hivepay.verifyWebhook({
payload: await req.text(),
signature: req.headers.get('X-HivePay-Signature')!,
timestamp: req.headers.get('X-HivePay-Timestamp')!,
maxAge: 300000 // Reject webhooks older than 5 minutes
});
if (!result.valid) {
console.error('Verification failed:', result.error);
return new Response(result.error, { status: 401 });
}
const { event } = result;
if (event.type === 'payment.status_changed') {
const { paymentId, status } = event.data;
if (status === 'completed') {
await fulfillOrder(paymentId);
}
}
return new Response('OK');
}
import { createHmac } from 'node:crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
apiKey: string,
timestamp: number,
maxAgeMs: number = 300000
): boolean {
// Reject old webhooks
const now = Date.now();
if (Math.abs(now - timestamp) > maxAgeMs) {
return false;
}
// Calculate expected signature
const signaturePayload = `${timestamp}.${payload}`;
const expectedSignature = createHmac('sha256', apiKey)
.update(signaturePayload)
.digest('hex');
// Timing-safe comparison
if (signature.length !== expectedSignature.length) {
return false;
}
let result = 0;
for (let i = 0; i < signature.length; i++) {
result |= signature.charCodeAt(i) ^ expectedSignature.charCodeAt(i);
}
return result === 0;
}
// Express handler
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-HivePay-Signature') || '';
const timestamp = parseInt(req.header('X-HivePay-Timestamp') || '0');
const body = req.body.toString();
if (!verifyWebhookSignature(body, signature, process.env.HIVEPAY_API_KEY!, timestamp)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(body);
console.log('Webhook received:', event.type);
res.status(200).send('OK');
});
#
Example Handlers
#
Express.js
import express from 'express';
import { HivePay } from '@hivepay/client';
const app = express();
const hivepay = new HivePay({ apiKey: process.env.HIVEPAY_API_KEY });
app.post('/webhooks/hivepay', express.raw({ type: 'application/json' }), async (req, res) => {
const result = await hivepay.verifyWebhook({
payload: req.body.toString(),
signature: req.header('X-HivePay-Signature')!,
timestamp: req.header('X-HivePay-Timestamp')!
});
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
const { event } = result;
switch (event.data.status) {
case 'completed':
await handlePaymentCompleted(event.data.id);
break;
case 'failed':
await handlePaymentFailed(event.data.id);
break;
}
res.json({ received: true });
});
#
Hono
import { Hono } from 'hono';
import { HivePay } from '@hivepay/client';
const app = new Hono();
const hivepay = new HivePay({ apiKey: process.env.HIVEPAY_API_KEY });
app.post('/webhooks/hivepay', async (c) => {
const result = await hivepay.verifyWebhook({
payload: await c.req.text(),
signature: c.req.header('X-HivePay-Signature')!,
timestamp: c.req.header('X-HivePay-Timestamp')!
});
if (!result.valid) {
return c.json({ error: result.error }, 401);
}
// Handle event...
return c.json({ received: true });
});
#
Testing Webhooks
#
Create Test Webhooks
const { signature, timestamp, body } = await hivepay.createTestWebhook({
type: 'payment.status_changed',
data: {
paymentId: 'pay_test_123',
status: 'completed'
}
});
// Send to your endpoint
await fetch('http://localhost:3000/webhooks/hivepay', {
method: 'POST',
headers: {
'X-HivePay-Signature': signature,
'X-HivePay-Timestamp': timestamp,
'Content-Type': 'application/json'
},
body
});
#
Local Development
Use the built-in test server:
pnpm test:webhook
This starts a local receiver at http://localhost:3137/webhook-receiver.
#
Security Best Practices
- Always verify signatures - Never process unverified webhooks
- Check timestamps - Reject webhooks older than 5 minutes
- Use HTTPS - Always use HTTPS in production
- Respond quickly - Return 2xx within 5 seconds; process async
- Handle duplicates - Implement idempotency for webhook handling
#
Retry Policy
Current Limitation
Webhooks are currently sent once without automatic retries. Failed deliveries are logged but not queued. A queue-based retry system is planned for future releases.
To handle missed webhooks:
- Implement a reconciliation process that periodically checks payment status
- Use the payment status API as a fallback