Agent skill
Mono Webhooks Skill
Receive and authenticate Mono webhooks. Use when setting up a Mono (open banking: Connect and DirectPay) webhook endpoint, verifying the mono-webhook-secret header, debugging 401s, or handling events like mono.events.account_connected, mono.events.account_updated, direct_debit.payment_successful and direct_debit.payment_failed.
Install this skill
npx skills add hookdeck/webhook-skills --skill mono-webhooks
Mono is an African open banking platform (Mono Connect for financial data, DirectPay for payments). It sends a JSON POST to your webhook URL when an account is connected, updated or unlinked, or when a DirectPay payment succeeds, fails, is cancelled or is abandoned. Webhooks are not signed. Every request carries a mono-webhook-secret header holding the webhook secret of your app, and your endpoint authenticates the request by comparing that header with the secret you have stored.
When to Use This Skill
- How do I receive Mono webhooks?
- How do I verify the
mono-webhook-secretheader? - Does Mono sign webhook payloads? (No: use the shared secret header)
- How do I handle
mono.events.account_orconnected direct_?debit.payment_ successful - Why does my Mono webhook endpoint return 401?
Setup Note
Use the webhook secret from your app in the Mono dashboard.
Set it as MONO_WEBHOOK_SECRET. See references/setup.md for where to find it.
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyMonoRequest(headers, secret = process.env.MONO_WEBHOOK_SECRET) {
if (!secret) return false; // never accept when unconfigured
const received = headers['mono-webhook-secret']; // the whole value is the secret
return Boolean(received) && safeEqual(received, secret);
}
- Header:
mono-webhook-secret: <secret>(plain text, no prefix). Compare the whole value. - Return
401on a missing or mismatched header,400for a body that isn't a JSON object, and exactly200once handled. Mono retries anything else with exponential backoff for 48 hours. - Read the body raw (
express.raw({ type: '*/*' })) so any content type works, authenticate, then parse. - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Serve the endpoint over HTTPS, rotate the secret if it leaks, and dedupe on
event_id.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Event Types
The body is { event, event_. event_id is reused when Mono retries a delivery, so dedupe on it.
| Event | Sent When |
|---|---|
mono.events.account_ | A user connects a financial account |
mono.events.account_ | Account data is refreshed or its status changes |
mono.events.account_ | A connected account is unlinked from your app |
direct_ | A one-time DirectPay debit completes |
direct_ | A bank failure or network downtime after the user authorized the debit |
direct_ | The user closes the DirectPay widget without authorizing |
direct_ | The user ignores the payment widget or the session times out |
Environment Variables
MONO_WEBHOOK_SECRET=your-webhook-secret # Mono dashboard > your app > Webhooks
Local Development
Mono can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 mono --path /webhooks/mono
Use the printed URL as the webhook URL in the Mono dashboard.
Resources
- references/overview.md - What Mono webhooks are, events and payload shape
- references/setup.md - Add the webhook URL and find the secret
- references/verification.md - Secret header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests