# 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)

# 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

  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