# Mollie Webhooks

## When to Use This Skill

* Setting up a Mollie webhook handler
* Understanding why Mollie webhooks have no signature to verify
* How do I confirm a Mollie payment status from a webhook?
* Handling payment status changes: `paid`, `authorized`, `canceled`, `expired`, `failed`
* Why does my Mollie webhook only contain an `id`?

## How Mollie Webhooks Work (Read This First)

Mollie webhooks are not signed — there is no HMAC, no signature header, and
no shared secret to verify. Instead of trusting the request, Mollie sends you a
POST with a single `application/x-www-form-urlencoded` body parameter:

```
id=tr_5B8cwPMGnU6qLbRvo7qEZo

```

The status is deliberately not in the payload. You must not trust the
request body — anyone could POST a fake `id`. Instead you fetch the resource
from the Mollie API using your API key and read the authoritative status. This
is the fetch-to-confirm pattern, and it is the security model: a forged
webhook can only ever cause you to re-fetch a real payment you own.

```
Mollie ──POST id=tr_xxx──▶  your endpoint
                              │
                              ▼
                    GET /v2/payments/tr_xxx  (with your API key)
                              │
                              ▼
                    read payment.status → act → return 200

```

## Verification (core) — fetch to confirm

There is no signature to check. The "verification" step is fetching the payment
from Mollie's API. Authenticate with your API key as a Bearer token
(`test_…` or `live_…`). Always return 200 quickly — even for an unknown or
deleted `id` — so Mollie stops retrying.

Node (official SDK, `@mollie/api-client`):

```javascript
const { createMollieClient } = require('@mollie/api-client');
const mollie = createMollieClient({ apiKey: process.env.MOLLIE_API_KEY });

// req.body.id came from the x-www-form-urlencoded webhook — do NOT trust it as status.
const payment = await mollie.payments.get(req.body.id); // 404 => unknown id, ack with 200
switch (payment.status) {                               // authoritative status from the API
  case 'paid': /* fulfill order */ break;
  case 'expired': case 'failed': case 'canceled': /* release order */ break;
}

```

Python (manual fetch — Mollie's official SDK is Node/PHP, so use the REST API):

```python
async with httpx.AsyncClient() as client:
    r = await client.get(
        f"https://api.mollie.com/v2/payments/{payment_id}",
        headers={"Authorization": f"Bearer {os.environ['MOLLIE_API_KEY']}"},
    )
# r.status_code == 404 => unknown id, acknowledge with 200
payment = r.json()          # authoritative status from the API
status = payment["status"]  # 'paid' | 'authorized' | 'canceled' | 'expired' | 'failed' | ...

```

> For complete handlers with route wiring, status dispatch, and tests, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mollie-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mollie-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/mollie-webhooks/examples/fastapi/)

## Common Payment Statuses

The webhook fires whenever a payment's status changes. Fetch the payment to read
which status it now has:

| Status | Meaning |
| --- | --- |
| `open` | Payment created, not yet paid |
| `pending` | Payment started, awaiting completion (some methods) |
| `authorized` | Amount reserved (two-step / pay-later methods) — capture to collect |
| `paid` | Payment successful — safe to fulfill |
| `canceled` | Customer or merchant canceled before completion |
| `expired` | Payment was not completed in time |
| `failed` | Payment attempt failed |

The webhook `id` prefix tells you the resource type: `tr_` = payment. Refunds and
chargebacks reuse the payment's webhook, so re-fetch the payment (and its refunds)
on any call.

> For the full status reference, see [Mollie payment status changes](https://docs.mollie.com/docs/payment-status-changes).

## Environment Variables

```bash
MOLLIE_API_KEY=test_xxxxx    # Mollie API key (test_… or live_…) from the dashboard

```

The same API key both creates payments (with a `webhookUrl`) and fetches them
in the webhook handler. There is no separate webhook secret.

## Local Development

```bash
# Start tunnel (no account needed) — forwards to your local handler
npx hookdeck-cli listen 3000 mollie --path /webhooks/mollie

```

Set the resulting public URL as the `webhookUrl` when you create a payment
(Mollie does not have a dashboard field for a global payments webhook — the
`webhookUrl` is set per payment via the API).

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mollie-webhooks/references/overview.md) - Mollie webhook concepts and statuses
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mollie-webhooks/references/setup.md) - Getting your API key and wiring `webhookUrl`
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/mollie-webhooks/references/verification.md) - The fetch-to-confirm pattern in detail