# 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.

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/qonto-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/qonto-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/qonto-webhooks/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](https://docs.qonto.com/api-reference/business-api/webhooks/supported-webhooks/v1-transactions) pages for all 17 types.

## Environment Variables

```bash
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

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qonto-webhooks/references/overview.md) - Qonto webhook concepts, event types
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qonto-webhooks/references/setup.md) - Subscription setup and secret handling
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/qonto-webhooks/references/verification.md) - Signature verification details and gotchas