# 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](/dashboard/settings/) or via API:

+++ TypeScript
```typescript
await hivepay.merchants.update('merchant_id', {
  webhookUrl: 'https://yoursite.com/webhooks/hivepay'
});
```
+++ cURL
```bash
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

```json
{
  "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](/dashboard/settings/) or via `POST /api/public/merchants/{id}/webhook-secret` (requires API key authentication).
- Regenerating the secret **immediately invalidates** the previous one.

!!!warning 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:

```js
HMAC_SHA256(webhookSecret, `${timestamp}.${JSON.stringify(payload)}`)
```

+++ TypeScript (SDK)
```typescript
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');
}
```
+++ Node.js (Manual)
```typescript
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

```typescript
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

```typescript
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

+++ TypeScript
```typescript
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:

```bash
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

!!!warning 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](/payments/tracking/) as a fallback
