Flutter 如何通过 webhook 完成 Payment confirmation
结论
webhook_url 不能直接指向 Flutter App。Webhook 是 Chargily 服务器发送到公网 HTTP/HTTPS 地址的服务端回调。Flutter App 通常没有固定的公网地址,而且可能已经退出、断网或进入后台休眠状态。
正确的处理流程如下:
- Flutter 请求后端创建支付。
- 后端调用 Chargily API,再把
checkout_url返回给 Flutter。 - 用户完成支付后,Chargily 向后端的
webhook_url发送请求。 - 后端验证回调并更新订单状态。
- Flutter 通过查询接口、WebSocket 或推送获取最终支付结果。
X-Authorization 中的 API Key 也不应保存在 Flutter App 中。客户端代码容易被反编译,因此支付接口应由后端调用。
整体流程
Flutter App
│
│ 1. 请求创建支付
▼
你的后端 ──────────► Chargily Invoice API
│ │
│ 2. checkout_url │
▼ │
Flutter App │
│ │
│ 3. 打开支付页面 │
▼ │
用户完成支付 │
│ 4. POST webhook
▼
你的后端
│
│ 5. 更新订单状态
▼
数据库
▲
│ 6. 查询支付状态
Flutter App ─────────────┘
这里需要区分两个概念:
- Chargily 发送到
webhook_url的数据位于 HTTP 请求的 body 中。 - Webhook 接口返回的 HTTP response 用于告诉 Chargily “回调已收到”。
Flutter 通常不需要获取 webhook 接口的原始 HTTP response,而应读取后端保存的订单状态。
后端接收 webhook
下面以 Node.js 和 Express 为例。回调字段应以实际收到的 Chargily 数据及对应版本的官方文档为准,所以示例不假定固定的字段名称。
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/chargily', async (req, res) => {
try {
const payload = req.body;
console.log('Chargily webhook:', payload);
// 按 Chargily 当前文档要求验证回调。
// 如果文档提供签名 header、签名算法或 webhook secret,
// 必须在更新订单前完成验证。
const verified = await verifyChargilyWebhook(req, payload);
if (!verified) {
return res.status(401).json({
received: false,
message: 'Invalid webhook',
});
}
// 字段名称仅作流程示意,必须替换成实际 webhook 字段。
const invoiceId = payload.invoice_id;
const status = payload.status;
if (!invoiceId) {
return res.status(400).json({
received: false,
message: 'Missing invoice identifier',
});
}
// 使用 invoiceId 查询本地订单,并以幂等方式更新状态。
await updatePaymentStatus(invoiceId, status, payload);
return res.status(200).json({
received: true,
});
} catch (error) {
console.error(error);
return res.status(500).json({
received: false,
});
}
});
async function verifyChargilyWebhook(req, payload) {
// 根据正在使用的 Chargily API 版本实现。
// 在没有确认官方签名规则之前,不要自行假设 header 或算法。
return true;
}
async function updatePaymentStatus(invoiceId, status, payload) {
// 写入数据库。
// 应保存原始 payload,并保证同一 webhook 重复到达时不会重复记账。
}
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
部署后,webhook_url 可以是:
https://api.example.com/webhooks/chargily
这个地址必须能够从公网访问,让 Chargily 服务器可以向它发送请求。生产环境应使用 HTTPS,不能使用以下地址:
localhost
127.0.0.1
192.168.x.x
在后端创建支付
创建支付时,应先生成自己的订单编号,再建立本地订单与 Chargily invoice 之间的关联。以下代码仅展示整体结构,具体请求参数仍以当前使用的 API 文档为准。
app.post('/payments', async (req, res) => {
try {
const { amount } = req.body;
const order = await createPendingOrder({
amount,
status: 'pending',
});
const response = await fetch(
'https://epay.chargily.com.dz/api/invoice',
{
method: 'POST',
headers: {
'X-Authorization': process.env.CHARGILY_API_KEY,
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount,
webhook_url: 'https://api.example.com/webhooks/chargily',
// 按 API 实际支持的字段传递本地订单标识。
// client_reference_id: order.id,
}),
},
);
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json(data);
}
await saveProviderInvoice(order.id, data);
return res.json({
orderId: order.id,
checkoutUrl: data.checkout_url,
});
} catch (error) {
console.error(error);
return res.status(500).json({
message: 'Unable to create payment',
});
}
});
API Key 应保存在服务端环境变量中:
CHARGILY_API_KEY=your_api_key
不要将 API Key 硬编码到 Flutter、提交到 Git 仓库,或作为普通参数返回给客户端。
Flutter 查询支付结果
后端收到 webhook 并更新订单后,Flutter 可以轮询自己的订单查询接口。
Future<String> getPaymentStatus(String orderId) async {
final response = await dio.get(
'https://api.example.com/payments/$orderId',
);
return response.data['status'] as String;
}
下面是一个简单的轮询示例:
Future<String> waitForPayment(String orderId) async {
const maxAttempts = 30;
for (var attempt = 0; attempt < maxAttempts; attempt++) {
final status = await getPaymentStatus(orderId);
if (status == 'paid' ||
status == 'failed' ||
status == 'cancelled') {
return status;
}
await Future<void>.delayed(const Duration(seconds: 2));
}
return 'pending';
}
状态名称应与自己的后端约定保持一致,不能直接假设 Chargily 一定会返回 paid、failed 或 cancelled。后端可以把支付平台的状态统一映射为应用内部状态。
如果需要实时更新,后端也可以通过 WebSocket、Server-Sent Events 或推送通知 Flutter。即便采用实时通知,App 恢复到前台时仍建议重新查询一次订单状态。
不要根据支付页面跳转判断成功
用户从支付页面跳回 App,只能说明页面发生了跳转,不能证明款项已经到账。用户可能中途关闭页面,跳转参数也可能被修改。网络延迟还可能导致页面跳转与实际入账不同步。
支付结果应以服务端确认的信息为准:
- 后端接收并验证 webhook。
- 必要时,后端调用 Chargily 的订单或 invoice 查询接口进行二次确认。
- 检查金额、币种、invoice 标识是否与本地订单一致。
- 在数据库事务中更新订单和发货状态。
本地调试 webhook
开发阶段可以使用隧道服务,临时把本地端口暴露到公网,例如:
https://temporary-domain.example/webhooks/chargily
然后把该地址配置为 webhook_url。调试时可以记录请求 headers 和 body,但不要把 API Key、签名密钥或用户敏感信息写入公开日志。
注意事项
- Webhook 可能会重复发送,因此处理逻辑必须保证幂等。同一个 invoice 只能入账或发货一次。
- 收到 webhook 后应尽快返回成功响应。耗时任务可以交给消息队列异步处理。
- 必须按照官方要求验证签名、密钥或其他真实性凭据。不能因为请求中包含某个 invoice ID 就直接信任它。
- 不要信任 Flutter 上报的 “支付成功” 状态、金额或订单编号。
- 创建支付和接收 webhook 都应由后端处理,以免泄露 API Key。
- Flutter 轮询的是自己的后端接口,并不是等待 webhook 直接进入手机。
- 只有支付状态、金额、币种和订单归属全部匹配后,才能执行充值、发货或开通服务。
备注:内容仅供参考。
