> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monigo.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time event notifications from Monigo.

Monigo sends webhooks to your server when important events occur — invoice paid, payout completed, customer wallet topped up, usage period stats, and more.

## Setting up a webhook endpoint

1. Go to **Dashboard → Webhooks → Add Endpoint**
2. Enter your HTTPS URL
3. Select the event types you want to receive
4. Copy the signing secret

## Verifying webhook signatures

Every Monigo webhook includes a `X-Monigo-Signature` header. Verify it before processing the request.

```typescript theme={null}
import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expected}`)
  );
}

// In your handler:
app.post('/webhooks/monigo', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-monigo-signature'] as string;
  if (!verifyWebhook(req.body.toString(), sig, process.env.MONIGO_WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }
  const event = JSON.parse(req.body);
  // handle event...
  res.status(200).send('ok');
});
```

```go theme={null}
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"os"
)

func verifyWebhook(payload []byte, signature, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(payload)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(signature), []byte(expected))
}

func handleMonigoWebhook(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "read error", http.StatusBadRequest)
		return
	}

	sig := r.Header.Get("X-Monigo-Signature")
	if !verifyWebhook(body, sig, os.Getenv("MONIGO_WEBHOOK_SECRET")) {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}

	var event map[string]any
	if err := json.Unmarshal(body, &event); err != nil {
		http.Error(w, "bad json", http.StatusBadRequest)
		return
	}
	// handle event...
	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("ok"))
}

func main() {
	http.HandleFunc("/webhooks/monigo", handleMonigoWebhook)
	_ = http.ListenAndServe(":8080", nil)
}
```

## Event types

### Payment

| Event             | Description                              |
| ----------------- | ---------------------------------------- |
| `payment.success` | A payment transaction succeeded          |
| `payment.failed`  | A payment transaction failed             |
| `payout.success`  | A payout transfer completed successfully |
| `payout.failed`   | A payout transfer failed                 |
| `payout.reversed` | A payout transfer was reversed           |

### Invoice

| Event               | Description                                              |
| ------------------- | -------------------------------------------------------- |
| `invoice.finalized` | An invoice was finalized (moved from draft to finalized) |
| `invoice.paid`      | An invoice was marked as paid after a successful payment |
| `invoice.voided`    | An invoice was voided                                    |

### Customer

| Event                                   | Description                                            |
| --------------------------------------- | ------------------------------------------------------ |
| `customer.wallet.topped_up`             | A customer's wallet was credited                       |
| `customer.payment_method.expired`       | A customer's payment method has expired                |
| `customer.payment_method.expiring_soon` | A customer's payment method is expiring within 30 days |

### Usage — org level

Fired once per organization per period (daily at 08:00 UTC, weekly on Mondays, monthly on the 1st).

| Event           | Description                                             |
| --------------- | ------------------------------------------------------- |
| `usage.daily`   | Yesterday's event ingestion stats for the org           |
| `usage.weekly`  | Last 7 days' event ingestion stats for the org          |
| `usage.monthly` | Last calendar month's event ingestion stats for the org |

### Usage — per customer

Fired once per customer that had non-zero activity in the period. Sent at the same time as the org-level events.

| Event                    | Description                                             |
| ------------------------ | ------------------------------------------------------- |
| `customer.usage.daily`   | Yesterday's event count for a single customer           |
| `customer.usage.weekly`  | Last 7 days' event count for a single customer          |
| `customer.usage.monthly` | Last calendar month's event count for a single customer |

### Subscription

| Event                            | Description                                                                              |
| -------------------------------- | ---------------------------------------------------------------------------------------- |
| `subscription.suspended`         | A subscription was suspended after dunning retries were exhausted                        |
| `subscription.usage_cap_reached` | A customer's usage crossed the included-units threshold on an overage or cap-priced plan |

***

## Event payload reference

All webhooks are delivered as `POST` requests with `Content-Type: application/json`. The body is a JSON object whose shape depends on the event type.

### `payment.success` / `payment.failed`

The payload mirrors the raw event received from your payment provider (Paystack, Flutterwave, or Monnify).

```json theme={null}
{
  "event": "charge.success",
  "data": {
    "reference": "pay_abc123",
    "amount": 500000,
    "currency": "NGN"
  }
}
```

### `payout.success` / `payout.failed` / `payout.reversed`

The payload mirrors the raw payout event from your payment provider.

### `invoice.finalized`

```json theme={null}
{
  "invoice_id": "01924f1e-...",
  "customer_id": "01924f1e-...",
  "subscription_id": "01924f1e-...",
  "amount": "5000.000000",
  "currency": "NGN",
  "period_start": "2026-02-01T00:00:00Z",
  "period_end": "2026-03-01T00:00:00Z"
}
```

### `invoice.paid`

```json theme={null}
{
  "invoice_id": "01924f1e-...",
  "customer_id": "01924f1e-...",
  "subscription_id": "01924f1e-...",
  "amount": "5000.000000",
  "currency": "NGN",
  "period_start": "2026-02-01T00:00:00Z",
  "period_end": "2026-03-01T00:00:00Z",
  "paid_at": "2026-02-14T10:32:00Z"
}
```

### `invoice.voided`

```json theme={null}
{
  "invoice_id": "01924f1e-...",
  "customer_id": "01924f1e-...",
  "subscription_id": "01924f1e-...",
  "amount": "5000.000000",
  "currency": "NGN",
  "period_start": "2026-02-01T00:00:00Z",
  "period_end": "2026-03-01T00:00:00Z"
}
```

### `customer.wallet.topped_up`

```json theme={null}
{
  "customer_id": "01924f1e-...",
  "wallet_id": "01924f1e-...",
  "amount": "10000.000000",
  "currency": "NGN",
  "entry_type": "deposit",
  "balance_after": "25000.000000"
}
```

### `customer.payment_method.expired` / `customer.payment_method.expiring_soon`

```json theme={null}
{
  "customer_id": "01924f1e-...",
  "payment_method_id": "01924f1e-...",
  "last4": "4242",
  "exp_month": 2,
  "exp_year": 2026,
  "card_type": "visa"
}
```

### `usage.daily`

```json theme={null}
{
  "period": "Feb 13, 2026",
  "events_yesterday": 14823,
  "events_mtd": 198450,
  "active_customers": 312
}
```

### `usage.weekly`

```json theme={null}
{
  "period": "2026-02-07/2026-02-14",
  "events_count": 98210,
  "active_customers": 540
}
```

### `usage.monthly`

```json theme={null}
{
  "period": "2026-01",
  "events_count": 1842300,
  "active_customers": 1204
}
```

### `customer.usage.daily` / `customer.usage.weekly` / `customer.usage.monthly`

```json theme={null}
{
  "customer_id": "01924f1e-...",
  "period": "Feb 13, 2026",
  "events_count": 423
}
```

### `subscription.suspended`

```json theme={null}
{
  "customer_id": "01924f1e-...",
  "customer_name": "Acme Corp",
  "subscription_id": "01924f1e-...",
  "invoice_id": "01924f1e-...",
  "amount": "5000.000000",
  "currency": "NGN"
}
```

### `subscription.usage_cap_reached`

Fired the first time a customer's usage crosses the `included_units` threshold on an `overage` or `cap`-priced plan within the current billing period. Fires at most once per subscription per billing period.

```json theme={null}
{
  "subscription_id": "01924f1e-...",
  "customer_id": "01924f1e-...",
  "plan_id": "01924f1e-...",
  "metric_id": "01924f1e-...",
  "pricing_model": "overage",
  "included_units": 10000,
  "current_usage": 10247,
  "period_start": "2026-02-01T00:00:00Z",
  "period_end": "2026-03-01T00:00:00Z"
}
```

***

## Retries

If your endpoint returns a non-2xx status, Monigo retries up to 5 times with exponential backoff:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes  |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours    |
| 5th retry | 8 hours    |

After 5 failed attempts, the delivery is marked as `failed` and no further retries occur. You can manually retry from the dashboard.

## Testing webhooks locally

use a tunnelling tool like [ngrok](https://ngrok.com) and register the public URL in your dashboard.
