PAYATHON 2026

如何创建自定义 UPI payment api 并验证交易?

支付老李

结论

可以自建 UPI 收款与交易验证 API,但这套 API 只能封装业务系统与收单银行之间的交互。一般商户无法像调用普通开放 API 一样直接连接 NPCI,也不能只凭 UPI ID、UTR 或付款截图向 NPCI 验证交易。

如果完全不使用第三方支付网关,可行的做法是:

  1. 与支持 UPI 商户收单的银行签约;
  2. 完成企业、网站/App、结算账户和合规审核;
  3. 获取银行分配的 merchant ID、VPA、密钥、证书和 API 权限;
  4. 通过银行 API 创建付款请求或动态 QR;
  5. 使用银行的异步通知和交易状态查询接口验证付款;
  6. 在自己的服务器上处理订单、验签、对账和退款。

换句话说,可以不使用第三方支付网关,但通常不能绕过银行或其他获准的 UPI 参与机构,直接调用 NPCI 的公共 API 收款。

为什么不能直接接入 NPCI

NPCI 负责运营 UPI 网络,但 UPI 不是向任意开发者开放的普通互联网支付 API。生产环境通常涉及以下角色:

  • issuer bank:付款人的发卡银行;
  • acquiring/beneficiary bank:为商户提供收款服务的银行;
  • PSP bank:提供 UPI 接入能力的银行;
  • TPAP:依托 PSP bank 提供 UPI App 服务的第三方应用;
  • merchant:通过收单银行或支付服务商接收付款。

直接成为 UPI 网络参与方,需要满足准入、合规、安全、审计、争议处理和技术认证等要求。这与注册开发者账号后获取 API key 完全不同。

NPCI 发布规范或开发资料,也不代表它向普通商户提供了可直接用于生产收款的公共 API。具体准入条件可能发生变化,应以 NPCI 和合作银行当时提供的正式材料为准。

推荐的系统结构

自建 API 可以采用下面的结构:

用户的 UPI App
      │
      ▼
UPI / 银行网络
      │
      ▼
商户收单银行
      │
      ├── Payment API
      ├── Status API
      ├── Refund API
      └── Webhook / Callback
              │
              ▼
       自建 UPI Payment API
              │
              ├── 订单数据库
              ├── 验签与幂等处理
              ├── 状态查询
              ├── 对账任务
              └── 业务系统

这层 API 可以统一不同银行的字段和状态,但资金处理和权威交易状态仍由签约银行提供,不能由本地代码自行判断。

实现步骤

1. 向银行申请 UPI 商户收单

联系银行的 corporate banking、merchant acquiring 或 API banking 团队,确认银行是否提供以下能力:

  • UPI merchant onboarding;
  • merchant VPA;
  • dynamic QR 或 intent payment;
  • payment status enquiry;
  • server-to-server callback;
  • refund;
  • settlement report;
  • sandbox/UAT 环境;
  • 签名或双向 TLS 认证。

在银行开放平台看到 API 文档,并不表示注册后就能自动获得生产权限。许多接口需要签署协议、通过审核,再由银行人工开通。

还要提前确认这些接口是否面向普通企业商户,而不是仅供银行、PSP、聚合支付机构或特定合作伙伴使用。

2. 为每次付款创建唯一订单

同一个金额加同一个固定二维码,无法可靠地识别订单。每次付款至少应生成:

  • merchant_order_id:商户侧唯一订单号;
  • amount:固定金额;
  • currency:通常为 INR;
  • customer_reference:可选的业务标识;
  • expires_at:付款请求有效期;
  • 银行返回的 bank_reference 或同类标识。

数据库可以保存以下状态:

CREATED
PENDING
SUCCESS
FAILED
EXPIRED
REFUNDED

订单号必须唯一。金额应使用定点小数或最小货币单位保存,不要用浮点数直接比较金额。

3. 调用银行的付款 API

各家银行的 URL、字段和签名规则并不统一。下面展示的只是自建服务结构,并非 NPCI 或任何一家银行的真实接口:

async function createUpiPayment(order) {
  const request = {
    merchantId: process.env.BANK_MERCHANT_ID,
    merchantOrderId: order.id,
    amount: order.amount,
    currency: "INR",
    callbackUrl: "https://merchant.example.com/webhooks/upi"
  };

  const signature = signBankRequest(request);

  const response = await fetch(`${process.env.BANK_API_BASE_URL}/payments`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Signature": signature
    },
    body: JSON.stringify(request)
  });

  if (!response.ok) {
    throw new Error(`Bank API returned HTTP ${response.status}`);
  }

  return response.json();
}

signBankRequest() 必须严格按照银行文档实现。银行可能要求 HMAC、RSA 签名、JWS、客户端证书或双向 TLS,商户不能自行选择算法。

付款接口一般会返回动态 QR 内容、支付链接、intent URI 或银行侧交易标识,具体形式由合作银行决定。

4. 接收并验证 webhook

收到 webhook 后,不能仅看到 status: SUCCESS 就发货。至少要完成以下检查:

  1. 按银行规定的方法验证签名;
  2. 检查通知中的商户身份;
  3. 查找本地订单;
  4. 核对订单号、金额和币种;
  5. 防止重复处理通知;
  6. 必要时再次调用银行的状态查询接口;
  7. 使用数据库事务更新订单。

下面仍是通用示例,字段和请求头需要按银行的实际规范替换:

app.post(
  "/webhooks/upi",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.get("X-Signature");
    const rawBody = req.body;

    if (!verifyBankSignature(rawBody, signature)) {
      return res.status(401).send("invalid signature");
    }

    const event = JSON.parse(rawBody.toString("utf8"));

    const order = await db.orders.findById(event.merchantOrderId);
    if (!order) {
      return res.status(404).send("unknown order");
    }

    if (
      event.currency !== "INR" ||
      event.amount !== order.amount ||
      event.merchantId !== process.env.BANK_MERCHANT_ID
    ) {
      return res.status(400).send("payment mismatch");
    }

    if (order.status === "SUCCESS") {
      return res.status(200).send("ok");
    }

    const bankStatus = await queryBankPaymentStatus(
      event.merchantOrderId,
      event.bankReference
    );

    if (
      bankStatus.status === "SUCCESS" &&
      bankStatus.amount === order.amount &&
      bankStatus.currency === "INR"
    ) {
      await db.transaction(async (tx) => {
        await tx.orders.markPaid(order.id, {
          bankReference: bankStatus.bankReference,
          paidAt: bankStatus.paidAt
        });

        await tx.outbox.insert({
          type: "ORDER_PAID",
          aggregateId: order.id
        });
      });
    }

    return res.status(200).send("ok");
  }
);

如果银行要求对原始 HTTP body 验签,就要在解析 JSON 前保留原始字节。重新序列化 JSON 可能改变空格、字段顺序或编码,导致验签失败。

5. 提供主动状态查询

Webhook 可能延迟、重复,或暂时无法送达,因此还需要实现主动状态查询:

app.get("/api/payments/:orderId", async (req, res) => {
  const order = await db.orders.findById(req.params.orderId);

  if (!order) {
    return res.status(404).json({ error: "ORDER_NOT_FOUND" });
  }

  if (order.status === "PENDING") {
    const result = await queryBankPaymentStatus(
      order.id,
      order.bankReference
    );

    await reconcileOrderWithBank(order, result);
  }

  const updatedOrder = await db.orders.findById(req.params.orderId);

  res.json({
    orderId: updatedOrder.id,
    status: updatedOrder.status,
    amount: updatedOrder.amount,
    currency: updatedOrder.currency
  });
});

前端只能查询自己的后端,不能让浏览器直接调用银行接口。密钥、证书和签名逻辑必须留在服务端。

什么才算付款验证成功

可信的成功状态应来自签约银行提供的 webhook、状态查询接口或结算文件,并且同时满足以下条件:

  • 商户订单号一致;
  • merchant ID 或 merchant VPA 一致;
  • 金额与币种一致;
  • 银行侧状态为最终成功;
  • 银行交易标识未被其他订单使用;
  • 通知签名有效;
  • 订单此前没有被支付或关闭。

以下信息不能单独作为付款成功的依据:

  • 用户上传的截图;
  • 用户填写的 UTR、RRN 或 transaction ID;
  • 前端跳转到了”支付完成”页面;
  • UPI App 显示了成功画面;
  • 客户端上报的成功状态;
  • 静态二维码被扫描;
  • 银行短信或人工提交的通知。

尤其不能只凭用户提供的 UTR 自动发货,因为该标识可能填写错误、被重复提交,甚至属于另一笔交易。能否通过 UTR 查询交易,还要看银行是否向该商户开放了相应接口。

UPI URI 不能代替交易验证

生成下面这样的 UPI URI,并不表示已经拥有 UPI payment API:

upi://pay?pa=merchant@bank&pn=Merchant&am=499.00&cu=INR&tr=ORDER123

它只能打开兼容的 UPI App,并预先填写付款信息。应用跳转回来时附带的客户端结果不能作为最终付款凭证,服务端仍需向收单银行确认交易状态。

URI 参数、商户标识、签名要求和可用的支付流程,也可能受到银行及 UPI 规则限制。正式上线前,应使用银行认可的生成方式,不能只靠自行拼接 URI。

必须处理的工程问题

幂等性

银行可能多次发送同一条通知。应为 merchant_order_id 和银行交易标识建立唯一约束,避免重复通知造成重复发货、充值或记账。

状态不能随意回退

订单一旦变为 SUCCESS,之后即使收到延迟的 PENDING 通知,也不应将其改回待支付。服务端需要明确规定状态转换规则。

超时不代表失败

调用银行 API 超时,只能说明结果未知。此时应让订单继续保持 PENDING,再通过状态查询或对账任务确认结果,不能立即用同一个订单号创建另一笔支付。

对账

至少每天获取一次银行的交易或结算报告,并与本地订单逐笔核对。Webhook 返回成功,不一定表示资金已经结算。交易成功、退款成功和资金结算应分别管理状态。

安全

至少应落实以下措施:

  • TLS;
  • 银行要求的 IP allowlist;
  • 双向 TLS 或请求签名;
  • 密钥轮换;
  • webhook 防重放;
  • 原始请求审计;
  • 敏感数据脱敏;
  • 最小权限控制;
  • 服务端时间同步;
  • 告警与人工补单流程。

不要把银行密钥、merchant secret 或私钥放进网页、移动 App 或公开代码仓库。

是否值得完全自建

对于单个商户和一家合作银行,自建一层 UPI API 是可行的。这样可以减少对第三方网关的依赖,订单和对账逻辑也能由自己控制。

但自建并不等于绕过支付参与方。商户仍需负责:

  • 与银行进行商务签约和技术联调;
  • 通过 UAT 与生产审核;
  • 管理证书和密钥;
  • 保障 API 可用性与容灾;
  • 处理 webhook 补偿;
  • 处理对账、退款和争议;
  • 维护安全与合规。

较稳妥的做法是先选择一家提供完整 UPI merchant acquiring API 的银行,索取正式的 onboarding、payment、status、callback、refund 和 reconciliation 文档,再围绕这些接口开发统一 API。如果银行只提供账户查询或企业转账 API,没有 UPI 商户收单能力,这些 API 就不能替代支付网关。

备注:内容仅供参考。