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-secret header?
  • Does Mono sign webhook payloads? (No: use the shared secret header)
  • How do I handle mono.events.account_connected or 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 401 on a missing or mismatched header, 400 for a body that isn't a JSON object, and exactly 200 once 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_id, timestamp, data }. event_id is reused when Mono retries a delivery, so dedupe on it.

EventSent When
mono.events.account_connectedA user connects a financial account
mono.events.account_updatedAccount data is refreshed or its status changes
mono.events.account_unlinkedA connected account is unlinked from your app
direct_debit.payment_successfulA one-time DirectPay debit completes
direct_debit.payment_failedA bank failure or network downtime after the user authorized the debit
direct_debit.payment_cancelledThe user closes the DirectPay widget without authorizing
direct_debit.payment_abandonedThe 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 10, 2026

View on GitHub →