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

Header Description
X-HivePay-Signature HMAC-SHA256 signature for verification
X-HivePay-Timestamp Unix timestamp (ms) when webhook was generated
Content-Type application/json

Payload

{
  "type": "payment.status_changed",
  "data": {
    "id": "cmj7b2rg10004d2rimvum8kaz",
    "merchantId": "cmkfgjmim00008rcxp81upef9",
    "status": "completed",
    "providerId": "hive",
    "internalId": "trx_abc123"
  }
}

Event Types

Event Description
payment.status_changed Payment status changed (most common)

Payment Statuses

Status Description
pending Payment created, awaiting user action
processing Payment initiated, awaiting confirmation
completed Payment successful
failed Payment failed
user_cancelled User cancelled payment
system_cancelled System cancelled (timeout/expired)

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.

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

  1. Always verify signatures - Never process unverified webhooks
  2. Check timestamps - Reject webhooks older than 5 minutes
  3. Use HTTPS - Always use HTTPS in production
  4. Respond quickly - Return 2xx within 5 seconds; process async
  5. Handle duplicates - Implement idempotency for webhook handling

Retry Policy

To handle missed webhooks:

  • Implement a reconciliation process that periodically checks payment status
  • Use the payment status API as a fallback