# Webhooks Overview

Webhooks notify your server in real time when payment events occur. Instead of polling the API, ZevPay sends an HTTP POST request to your configured webhook URL.

## How webhooks work

1. A payment event occurs (e.g., customer completes a transfer)
2. ZevPay sends a POST request to the webhook URL configured on your API key
3. Your server processes the event and responds with a `2xx` status code

```
Customer pays → ZevPay confirms → POST to your webhook URL → You fulfill the order
```

## Setting up webhooks

Configure your webhook URL in the ZevPay Dashboard under **Settings → API Keys**. Each API key pair (secret + public) shares a webhook URL and webhook secret.

### Requirements

- Your endpoint must be publicly accessible (HTTPS recommended for production)
- Your endpoint must respond with a `2xx` status code within 30 seconds
- Your endpoint should accept `POST` requests with `Content-Type: application/json`

## Webhook payload

All webhook payloads follow this structure:

```json
{
  "event": "charge.success",
  "data": {
    "reference": "ZVP-CKO-S-abc123",
    "amount": 500000,
    "currency": "NGN",
    "status": "completed",
    // ... event-specific fields
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `event` | string | The event type (e.g., `"charge.success"`) |
| `data` | object | Event-specific payload |

## Security

Every webhook request includes an HMAC-SHA256 signature in the `x-zevpay-signature` header. **Always verify this signature** before processing the event.

```
x-zevpay-signature: a1b2c3d4e5f6...
```

The signature is computed using your webhook secret (available in the dashboard). See [Verifying Signatures](./signatures) for implementation details.

## Webhook delivery

| Property | Value |
|----------|-------|
| **Method** | POST |
| **Content-Type** | application/json |
| **Timeout** | 30 seconds |
| **Signature header** | `x-zevpay-signature` |
| **Max retries** | 5 |
| **Deduplication** | Per event + reference |

### Automatic retries

If your server fails to respond with a `2xx` status code (or doesn't respond within 30 seconds), ZevPay will automatically retry the delivery with exponential backoff:

| Retry | Delay after previous attempt |
|-------|------------------------------|
| 1st retry | ~4 minutes |
| 2nd retry | ~9 minutes |
| 3rd retry | ~16 minutes |
| 4th retry | ~25 minutes |
| 5th retry | ~36 minutes |

After 5 failed retries, the webhook is marked as permanently failed. You can manually retry failed webhooks from the dashboard.

::: warning
Make sure your webhook endpoint is reliable and responds quickly. If your server is consistently unresponsive, webhook deliveries will be lost after the retry window expires.
:::

### Deduplication

ZevPay prevents duplicate webhook deliveries. If a webhook for the same event and transaction reference has already been successfully delivered to your endpoint, it will not be sent again. This protects against accidental double-processing on our end.

However, you should still implement idempotent handlers on your side — see [Handle duplicates](#handle-duplicates) below.

### Delivery logging

All webhook deliveries are logged with:
- Delivery status (`delivered`, `failed`, or `pending`)
- HTTP status code from your server
- Response time in milliseconds
- Retry count and last attempt timestamp
- Request and response body

You can view webhook delivery logs in the dashboard.

## Best practices

### Respond quickly

Return a `2xx` response as fast as possible. Process the event asynchronously if needed:

```js
app.post('/webhooks/zevpay', (req, res) => {
  // Respond immediately
  res.status(200).send('OK');

  // Process asynchronously
  processEvent(req.body).catch(console.error);
});
```

### Handle duplicates

Design your webhook handler to be **idempotent**. Use the `reference` field to detect and skip duplicate events:

```js
async function processEvent(payload) {
  const existing = await db.findByReference(payload.data.reference);
  if (existing) return; // Already processed

  // Process the event
  await fulfillOrder(payload.data);
}
```

### Verify signatures

Always verify the `x-zevpay-signature` header before processing events. See [Verifying Signatures](./signatures).

### Use HTTPS

Always use HTTPS for your webhook endpoint in production to prevent payload interception.

## Events reference

| Event | Description |
|-------|-------------|
| `charge.success` | Payment completed successfully |

See [Events Reference](./events) for detailed payload documentation.
