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
Webhook Secret
Each merchant receives a dedicated webhook signing secret (whsec_xxx) at registration. This secret is separate from your API key and is used exclusively for webhook signature verification.
- The webhook secret is returned once during registration. Store it securely alongside your API key.
- You can regenerate it in the Dashboard settings or via
POST /api/public/merchants/{id}/webhook-secret(requires API key authentication). - Regenerating the secret immediately invalidates the previous one.
Save Your Webhook Secret
The webhook secret is shown only once — during registration or after regeneration. If you lose it, you'll need to regenerate a new one (which invalidates the old one).
Signature Verification
Always verify webhook signatures to ensure authenticity. The signature is computed as:
HMAC_SHA256(webhookSecret, `${timestamp}.${JSON.stringify(payload)}`)
import { HivePay } from '@hivepay/client';
const hivepay = new HivePay({
apiKey: 'sk_live_xxx',
webhookSecret: 'whsec_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,
webhookSecret: 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', webhookSecret)
.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_WEBHOOK_SECRET!, timestamp)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(body);
// Handle event...
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,
webhookSecret: process.env.HIVEPAY_WEBHOOK_SECRET
});
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,
webhookSecret: process.env.HIVEPAY_WEBHOOK_SECRET
});
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