PAYATHON 2026

支持 INR 并可用于 nodejs 应用的 Payment gateway

支付阿杰

结论

Node.js 应用接收 INR 付款时,可以考虑以下支付网关:

  • Razorpay:Node.js 接入文档较完整,支持 INR、Hosted Checkout、银行卡、UPI、Netbanking 等,适合大多数印度本地业务。
  • Cashfree Payments:支持 INR 和多种印度本地支付方式,并提供服务端 API 和托管收银台。
  • PayU India:主要面向印度市场,接入流程和商户审核可能更偏向企业客户。
  • CCAvenue:覆盖多种支付方式,通常会跳转到托管支付页面完成付款。
  • Stripe India:技术接口成熟,但印度商户的开户条件、收款能力及跨境交易限制需要根据当前政策确认。

如果没有特殊的银行渠道或企业合同要求,可以优先评估 Razorpay 和 Cashfree Payments。两者都适合采用这样的接入方式:Node.js 服务端创建订单,浏览器打开托管收银台,服务端验证回调。

支付产品、开户条件和监管要求可能发生变化。正式选型前,应以网关当前的官方文档和商户审核结果为准。

推荐的接入流程

支付订单不能直接由浏览器创建,API Secret 也不能放进前端代码。建议采用以下流程:

  1. 用户在应用中提交订单。
  2. Node.js 服务端根据数据库中的商品价格计算金额。
  3. 服务端调用支付网关 API 创建支付订单。
  4. 前端使用返回的订单 ID 打开支付网关的 Hosted Checkout。
  5. 支付完成后,前端将支付结果提交给服务端。
  6. 服务端验证签名,并通过 Webhook 或支付查询接口确认最终状态。
  7. 验证成功后,再将业务订单标记为已支付。

支付是否成功必须由服务端确认,不能直接采用前端传回的状态。

Razorpay 的 Node.js 示例

下面使用 Razorpay Orders API 展示基本的接入结构。生产环境还需要加入身份认证、数据库事务、幂等控制和 Webhook 处理。

安装依赖:

npm install express razorpay dotenv

环境变量:

RAZORPAY_KEY_ID=your_key_id
RAZORPAY_KEY_SECRET=your_key_secret

创建订单并验证支付签名:

import "dotenv/config";
import crypto from "node:crypto";
import express from "express";
import Razorpay from "razorpay";

const app = express();

app.use(express.json());

const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID,
  key_secret: process.env.RAZORPAY_KEY_SECRET,
});

app.post("/api/payments/order", async (req, res) => {
  try {
    // 生产环境应根据 productId 或业务订单从数据库读取价格,
    // 不要直接采用客户端提交的金额。
    const amountInRupees = 499;
    const amountInPaise = amountInRupees * 100;

    const order = await razorpay.orders.create({
      amount: amountInPaise,
      currency: "INR",
      receipt: `receipt_${Date.now()}`,
    });

    // 此处应保存 order.id、业务订单号、金额和初始状态。
    res.json({
      keyId: process.env.RAZORPAY_KEY_ID,
      orderId: order.id,
      amount: order.amount,
      currency: order.currency,
    });
  } catch (error) {
    console.error("Failed to create payment order:", error);
    res.status(500).json({
      message: "Unable to create payment order",
    });
  }
});

app.post("/api/payments/verify", (req, res) => {
  const {
    razorpay_order_id,
    razorpay_payment_id,
    razorpay_signature,
  } = req.body;

  if (
    !razorpay_order_id ||
    !razorpay_payment_id ||
    !razorpay_signature
  ) {
    return res.status(400).json({
      verified: false,
      message: "Missing payment fields",
    });
  }

  const payload = `${razorpay_order_id}|${razorpay_payment_id}`;

  const expectedSignature = crypto
    .createHmac("sha256", process.env.RAZORPAY_KEY_SECRET)
    .update(payload)
    .digest("hex");

  const received = Buffer.from(razorpay_signature, "utf8");
  const expected = Buffer.from(expectedSignature, "utf8");

  const verified =
    received.length === expected.length &&
    crypto.timingSafeEqual(received, expected);

  if (!verified) {
    return res.status(400).json({
      verified: false,
      message: "Invalid payment signature",
    });
  }

  // 此处还应核对数据库中的 orderId、金额和币种,
  // 并等待 Webhook 或调用查询 API 确认最终支付状态。
  return res.json({ verified: true });
});

app.listen(3000, () => {
  console.log("Server listening on http://localhost:3000");
});

金额通常需要使用最小货币单位。对于 INR,₹499.00 应以 49900 paise 提交。处理金额时应避免使用浮点数。

前端打开托管收银台

页面加载 Razorpay Checkout 脚本后,先向自己的服务端请求创建订单,再打开支付窗口:

<script src="https://checkout.razorpay.com/v1/checkout.js"></script>

<button id="pay-button">Pay ₹499</button>

<script>
  document
    .getElementById("pay-button")
    .addEventListener("click", async () => {
      const orderResponse = await fetch("/api/payments/order", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        }
      });

      if (!orderResponse.ok) {
        throw new Error("Could not create payment order");
      }

      const order = await orderResponse.json();

      const checkout = new Razorpay({
        key: order.keyId,
        order_id: order.orderId,
        amount: order.amount,
        currency: order.currency,
        name: "Example Store",
        description: "Order payment",
        handler: async function (payment) {
          const verifyResponse = await fetch("/api/payments/verify", {
            method: "POST",
            headers: {
              "Content-Type": "application/json"
            },
            body: JSON.stringify(payment)
          });

          const result = await verifyResponse.json();

          if (result.verified) {
            window.location.href = "/payment/success";
          } else {
            window.location.href = "/payment/failed";
          }
        }
      });

      checkout.open();
    });
</script>

浏览器端的 handler 被调用,只能说明前端收到了支付结果,不能将其作为发货或开通服务的依据。

Webhook 也要验证

用户付款后可能立即关闭页面,导致前端没有调用 /api/payments/verify。因此,生产环境还需要配置 Webhook。

验证 Webhook 时通常需要:

  • 保留未经 JSON 解析的原始请求体。
  • 使用单独配置的 Webhook Secret 计算 HMAC。
  • 验证请求头中的签名。
  • 核对事件中的订单号、金额、币种和支付状态。
  • 使用事件 ID 或支付 ID 进行幂等处理。
  • 在数据库事务中更新业务订单。

Razorpay 的支付结果签名使用 API Secret,Webhook 通常使用单独设置的 Webhook Secret,不能混用。具体的事件名称、请求头和验签格式应以当前官方文档为准。

如何在几个网关之间选择

选型时不能只看网关是否写着“支持 INR”,还需要确认:

  • 是否接受你的公司注册国家和经营主体类型。
  • 是否支持印度境内交易、国际卡或跨境 INR 交易。
  • 是否提供业务所需的 UPI、Netbanking、银行卡和钱包等渠道。
  • 结算周期、退款费用、拒付处理方式和最低交易费用。
  • 是否支持订阅、自动扣款、部分退款和分账。
  • 是否提供 Hosted Checkout,以免自行处理银行卡敏感信息。
  • Webhook 是否可靠,能否查询支付的最终状态。
  • 测试环境与正式环境有哪些行为差异。
  • 商户审核需要提交哪些 KYC、银行账户、税务及公司材料。

如果业务主体不在印度,即使某个网关支持 INR,也不代表该主体可以用 INR 收款,或能向印度客户提供全部本地支付方式。开发前必须向网关确认这一点。

常见问题

不要把 Secret 放进前端

key_id 一般可以用于初始化 Checkout,key_secret 必须只保存在服务端。不要将它写入浏览器代码、移动端包、公开仓库或日志。

不要相信客户端金额

客户端请求可能被篡改。服务端应根据商品 ID、优惠规则和数据库订单重新计算应付金额,并在支付完成后再次核对。

使用幂等处理

Webhook 可能重复发送,客户端也可能重复提交。数据库中应对支付 ID、网关订单 ID 或业务订单号设置唯一约束,防止重复发货或重复开通服务。

区分业务订单和支付订单

建议分别保存:

business_order_id
gateway_order_id
gateway_payment_id
amount
currency
payment_status
created_at
paid_at

业务订单状态也不应只用一个布尔值表示。可以设置为 pending、paid、failed、refunded 等明确状态。

不要仅依赖页面跳转

成功页面、失败页面和浏览器回调都可能被伪造或中断。最终状态应来自验签通过的 Webhook,或由服务端调用支付查询 API 获取。

实际建议

可以先在 Razorpay 或 Cashfree 的测试环境中跑通以下最小流程:

  1. 服务端创建 INR 订单。
  2. 前端打开 Hosted Checkout。
  3. 服务端验证支付签名。
  4. Webhook 更新数据库状态。
  5. 测试成功、失败、取消、重复通知和退款场景。

确认商户主体能够通过审核、所需支付方式可以开通,并且结算条款可以接受后,再确定最终使用的网关。这样能避免技术接入已经完成,却因为开户或合规条件无法上线。

备注:内容仅供参考。