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.
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
- Go to Dashboard → Webhooks → Add Endpoint
- Enter your HTTPS URL
- Select the event types you want to receive
- Copy the signing secret
Verifying webhook signatures
Every Monigo webhook includes a X-Monigo-Signature header. Verify it before processing the request.
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');
});
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).
{
"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
{
"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
{
"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
{
"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
{
"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
{
"customer_id": "01924f1e-...",
"payment_method_id": "01924f1e-...",
"last4": "4242",
"exp_month": 2,
"exp_year": 2026,
"card_type": "visa"
}
usage.daily
{
"period": "Feb 13, 2026",
"events_yesterday": 14823,
"events_mtd": 198450,
"active_customers": 312
}
usage.weekly
{
"period": "2026-02-07/2026-02-14",
"events_count": 98210,
"active_customers": 540
}
usage.monthly
{
"period": "2026-01",
"events_count": 1842300,
"active_customers": 1204
}
customer.usage.daily / customer.usage.weekly / customer.usage.monthly
{
"customer_id": "01924f1e-...",
"period": "Feb 13, 2026",
"events_count": 423
}
subscription.suspended
{
"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.
{
"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 and register the public URL in your dashboard.