Agent skill

Qonto Webhooks Skill

Receive and verify Qonto webhooks. Use when setting up Qonto Business API webhook subscriptions, debugging X-Qonto-Signature verification, or handling events like v1/transactions, v1/cards, and v1/transfers.

Install this skill

npx skills add hookdeck/webhook-skills --skill qonto-webhooks


When to Use This Skill

  • How do I receive Qonto webhooks?
  • How do I verify the X-Qonto-Signature header (t=...,v1=...)?
  • Why does my Qonto webhook signature verification fail?
  • How do I handle v1/transactions, v1/cards or v1/transfers events?
  • How do I register a webhook subscription with POST /v2/webhook_subscriptions?

Verification (core)

Qonto signs each delivery with the subscription's secret. The X-Qonto-Signature header is t={timestamp},v1={signature}; the signature is lowercase hex HMAC-SHA256 of {t}.{raw body}. Use the raw body bytes, and use t exactly as it appears in the header.

const HEX64 = /^[0-9a-fA-F]{64}$/;
const parts = header.split(',').map((p) => p.trim());
const t = parts.find((p) => p.startsWith('t='))?.slice(2);
const candidates = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3)); // ignore v0 etc.
if (!t || !candidates.some((c) => HEX64.test(c))) throw new Error('bad header');

const expected = Buffer.from(
  crypto.createHmac('sha256', Buffer.from(secret, 'utf8'))   // whole secret, as a plain key
    .update(Buffer.concat([Buffer.from(`${t}.`), rawBody])) // rawBody is a Buffer
    .digest('hex'));
const ok = candidates.some((c) => HEX64.test(c) && // validate before comparing
  crypto.timingSafeEqual(Buffer.from(c.toLowerCase()), expected)); // accept if ANY v1 matches

Key points:

  • Secret: Paste the whole secret, including the whsec_ prefix. Qonto uses it as a plain HMAC key. It looks like a Standard Webhooks whsec_ secret but is not one: nothing is stripped or base64-decoded, and the standardwebhooks / svix libraries will not verify it.
  • The header can carry more than one v1= entry (key rotation); accept if any matches and ignore other keys such as v0. No t= value means fail.
  • Qonto documents no timestamp tolerance, and Hookdeck retries and replays keep the original t. Do not reject old timestamps by default; set QONTO_WEBHOOK_TOLERANCE_SECONDS (unset or 0 = off) to opt in.
  • Missing, malformed or mismatched signature: respond 401.
  • Qonto waits 1 second for a 2xx and retries with exponential backoff for a few days, so acknowledge first and process later.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/. They also include Qonto's published test vector (test-secret, {"test":"data"}, t=1704110400).

Common Event Types

Dispatch on the body's top-level type (the subscription type). What happened to the resource is data.event (for example created).

typeDescription
v1/transactionsBank transactions (data.event: created, updated, settled, ...)
v1/cardsCard lifecycle (created, ...)
v1/transfersSEPA transfers (created, ...)
v1/accountsBank accounts
v1/memberships, v1/organizationsMembers and organization status
v1/payment-links, v1/terminal-paymentsPayment links and terminal payments
v1/beneficiaries, v1/clients, v1/products, v1/client-invoicesBeneficiaries and invoicing resources

Qonto's "Supported events" lists are inconsistent about data.event: some pages list prefixed names (transactions.created) while their own examples carry the bare value ("event": "created"). Treat the examples as the truth and log values you don't recognise. See Qonto's supported webhooks pages for all 17 types.

Environment Variables

QONTO_WEBHOOK_SECRET=whsec_xxxxx             # Subscription secret, used whole as the HMAC key
QONTO_WEBHOOK_TOLERANCE_SECONDS=0            # Optional; unset or 0 = no timestamp check

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 qonto --path /webhooks/qonto

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →