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-Signatureheader (t=...,v1=...)? - Why does my Qonto webhook signature verification fail?
- How do I handle
v1/transactions,v1/cardsorv1/transfersevents? - 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 thestandardwebhooks/svixlibraries will not verify it. - The header can carry more than one
v1=entry (key rotation); accept if any matches and ignore other keys such asv0. Not=value means fail. - Qonto documents no timestamp tolerance, and Hookdeck retries and replays keep the original
t. Do not reject old timestamps by default; setQONTO_(unset orWEBHOOK_ TOLERANCE_ SECONDS 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).
type | Description |
|---|---|
v1/transactions | Bank transactions (data.event: created, updated, settled, ...) |
v1/cards | Card lifecycle (created, ...) |
v1/transfers | SEPA transfers (created, ...) |
v1/accounts | Bank accounts |
v1/memberships, v1/organizations | Members and organization status |
v1/payment-links, v1/terminal-payments | Payment links and terminal payments |
v1/beneficiaries, v1/clients, v1/products, v1/client-invoices | Beneficiaries 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
- references/overview.md - Qonto webhook concepts, event types
- references/setup.md - Subscription setup and secret handling
- references/verification.md - Signature verification details and gotchas