PAYATHON 2026

Flutter 如何通过 webhook 完成 Payment confirmation

支付阿杰

结论

webhook_url 不能直接指向 Flutter App。Webhook 是 Chargily 服务器发送到公网 HTTP/HTTPS 地址的服务端回调。Flutter App 通常没有固定的公网地址,而且可能已经退出、断网或进入后台休眠状态。

正确的处理流程如下:

  1. Flutter 请求后端创建支付。
  2. 后端调用 Chargily API,再把 checkout_url 返回给 Flutter。
  3. 用户完成支付后,Chargily 向后端的 webhook_url 发送请求。
  4. 后端验证回调并更新订单状态。
  5. 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,只能说明页面发生了跳转,不能证明款项已经到账。用户可能中途关闭页面,跳转参数也可能被修改。网络延迟还可能导致页面跳转与实际入账不同步。

支付结果应以服务端确认的信息为准:

  1. 后端接收并验证 webhook。
  2. 必要时,后端调用 Chargily 的订单或 invoice 查询接口进行二次确认。
  3. 检查金额、币种、invoice 标识是否与本地订单一致。
  4. 在数据库事务中更新订单和发货状态。

本地调试 webhook

开发阶段可以使用隧道服务,临时把本地端口暴露到公网,例如:

https://temporary-domain.example/webhooks/chargily

然后把该地址配置为 webhook_url。调试时可以记录请求 headers 和 body,但不要把 API Key、签名密钥或用户敏感信息写入公开日志。

注意事项

  • Webhook 可能会重复发送,因此处理逻辑必须保证幂等。同一个 invoice 只能入账或发货一次。
  • 收到 webhook 后应尽快返回成功响应。耗时任务可以交给消息队列异步处理。
  • 必须按照官方要求验证签名、密钥或其他真实性凭据。不能因为请求中包含某个 invoice ID 就直接信任它。
  • 不要信任 Flutter 上报的 “支付成功” 状态、金额或订单编号。
  • 创建支付和接收 webhook 都应由后端处理,以免泄露 API Key。
  • Flutter 轮询的是自己的后端接口,并不是等待 webhook 直接进入手机。
  • 只有支付状态、金额、币种和订单归属全部匹配后,才能执行充值、发货或开通服务。

备注:内容仅供参考。