Mollie Webhooks 事件回调配置 - 支付事件实时推送
Webhook 是什么
Webhook 是 Mollie 主动向你的服务器发送的 HTTP POST 请求。每当 Mollie 这边有事件发生(比如"某笔支付成功了"、"某个订阅被取消了"、"收到一笔拒付"),Mollie 的服务器就把这个事件打包成一个 HTTP POST 发到你配置的 URL。
这和你"主动去 Mollie 服务器查询"(Polling)的区别是:你不用轮询,事件来了 Mollie 自己推给你——实时、高效、不会漏掉。
为什么不能用 Redirect URL 代替 Webhook
很多新手以为"客户付款后 Mollie 会重定向回我的网站,我在那里处理就行"——这不够。
| 场景 | Redirect URL | Webhook |
|---|---|---|
| 客户付款成功后正常跳转 | ✅ 能收到 | ✅ 能收到 |
| 客户付款成功后关闭浏览器 | ❌ 收不到 | ✅ 能收到 |
| 3DS 验证后跳转失败 | ❌ 收不到 | ✅ 能收到 |
| 订阅自动扣费成功 | 不触发重定向 | ✅ 能收到 |
| 拒付 Chargeback | 不触发重定向 | ✅ 能收到 |
| 退款完成 | 不触发重定向 | ✅ 能收到 |
结论:Redirect URL 是给客户看的,Webhook 才是给你的服务器用来确认支付状态的唯一可靠信号。
配置步骤
Dashboard 里添加 Webhook
Dashboard → Developers → Webhooks → [Create Webhook]
│
├── Name: "My App Production Webhook"(给你自己看的名字)
├── URL: "https://myapp.com/api/mollie/webhook"(你的服务器地址,必须 HTTPS)
│
└── Payload 风格选择:
├── "ID only"(默认,最简洁)→ POST body 只有 payment id
└── "Full payload"(详细)→ POST body 包含完整的 payment 对象
配置要求
| 要求 | 原因 |
|---|---|
| 必须 HTTPS | Mollie 不会向 HTTP URL 推送 |
| 必须公网可访问 | localhost 不行,需要真实域名 |
| 必须返回 HTTP 200 | 且在 10 秒内返回 |
| 幂等处理 | 同一个 Webhook 可能重复推送(网络抖动时 Mollie 重试) |
你的 Webhook 处理器逻辑
收到 Webhook 请求后,你的服务器应该:
1. 验证请求来源(可选但推荐)
│
2. 从 body 里提取 payment id / order id / subscription id 等
│
3. 立即返回 HTTP 200(不要等后面的处理完成!)
│
4. 用提取的 id 调用 Mollie API 获取最新状态
│
5. 根据状态更新你的本地数据库
│
└── payment.paid → 订单状态改为"已付款"
payment.failed → 标记付款失败
subscription.canceled → 标记订阅取消
chargeback.received → 记录拒付
为什么要"立即返回 200"
Mollie 有一个10 秒超时:如果 10 秒内没收到你的 200 响应,Mollie 会认为推送失败,稍后自动重试(最多 5 次)。如果你的处理逻辑很耗时(比如要调用其他外部 API),应该:
- 收到 Webhook → 先存到队列里 → 返回 200 → 然后后台异步处理队列
Python 示例(Flask)
import os
from flask import Flask, request
app = Flask(__name__)
@app.route("/api/mollie/webhook", methods=["POST"])
def mollie_webhook():
# 1. 提取 id
data = request.get_json() # 或者 request.form.get("id")
payment_id = data.get("id")
if not payment_id:
return "", 400
# 2. 立即返回 200!不要等下面处理完
# Flask 默认在函数返回时发送响应,但异步处理更好
# 3. 用 payment_id 查询最新状态(异步处理)
import threading
def handle_payment(pid):
# 调用 Mollie API 获取 payment 状态
# 根据状态更新你的订单系统
pass
threading.Thread(target=handle_payment, args=(payment_id,)).start()
return "", 200
if __name__ == "__main__":
app.run()
幂等性处理
同一个 Webhook 事件可能被推送多次。你的处理逻辑必须是幂等的——多次收到同一个事件,最终结果和收到一次一样。
为什么会重复推送
- 你的服务器响应慢(>10 秒),Mollie 认为失败,稍后重试
- 网络抖动,请求可能被 Mollie 重发
- Mollie 的某些事件(比如订阅扣费)会触发多个 Webhook
怎么做幂等
-- 在订单表里加一个 webhook_processed 字段
UPDATE orders
SET status = 'paid', webhook_processed = true
WHERE id = ? AND webhook_processed IS NULL;
-- 用 affected_rows 判断
-- 如果返回 0,说明这条 Webhook 已经处理过了,跳过
-- 如果返回 1,正常处理
常见 Webhook 事件类型
| 事件 | 触发 | 对应对象 |
|---|---|---|
payment.created | Payment 创建时 | Payment |
payment.paid | 支付成功时 | Payment |
payment.failed | 支付失败时 | Payment |
payment.expired | 支付超时未完成时 | Payment |
payment.canceled | 支付被取消时 | Payment |
payment.refunded | 退款完成时 | Payment |
subscription.created | 订阅创建时 | Subscription |
subscription.canceled | 订阅取消时 | Subscription |
subscription.activated | 订阅激活时 | Subscription |
chargeback.received | 收到拒付时 | Payment |
order.created | Order 创建时 | Order |
order.paid | Order 支付成功时 | Order |
order.refunded | Order 退款时 | Order |
测试 Webhook
方法一:Dashboard 手动触发
Dashboard → Developers → Webhooks → 选你的 Webhook → "Send test event"。会向你的 URL 推送一个模拟事件。
方法二:用测试环境
在测试环境里完成一笔"付款"(Mock Payment),Mollie 会像正式环境一样推送 Webhook 到你的测试 Webhook URL。
方法三:内网穿透
本地开发时可以用 ngrok / localtunnel 把 localhost 暴露到公网,然后把穿透后的 URL 填到 Dashboard 里。这样 Mollie 就能推到你本地服务器上。
安全:验证 Webhook 来源
Mollie 在每个 Webhook 请求里放了一个 Mollie-Signature HTTP 头,内容是 HMAC-SHA256 签名。你可以用 API Key 来验证这个签名,确保请求来自 Mollie 而不是伪造的。
import hmac
import hashlib
api_key = "live_xxxxxxxxxxxxxxxx"
signature = request.headers.get("Mollie-Signature")
raw_body = request.get_data()
# Mollie 用 API Key 作为 HMAC 密钥
expected = hmac.new(api_key.encode(), raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
# 签名不匹配,可能是伪造的请求
return "", 400
继续阅读:SDK 与预置插件 →