印度移动 Wallet 应用应选择哪种 payment gateway?
明确结论
不要指望一个 payment gateway 包办所有功能。支付网关通常只处理用户充值时的收款,不能自动赋予应用保存用户资金、运营 Wallet、处理用户间转账或向银行账户提现的资格。
如果 Wallet 以印度为运营基础,较稳妥的方案是:
- 与印度受监管的银行或 RBI 授权的 PPI(Prepaid Payment Instrument)发行方合作,由合作方负责 Wallet 账户、KYC、资金托管和合规。
- 接入支持印度本地支付方式的 Payment Aggregator/Gateway,处理银行卡、net banking、UPI 等充值。
- 使用合作银行或合规支付服务商的 Payout API,通过 IMPS、NEFT、RTGS 或 UPI 等通道完成提现。
- 在应用内部建立独立的双重记账账本,记录用户间的 Wallet 余额转移。
- 单独设计国际用户和跨境资金流。不能因为某个印度网关支持国际卡,就认为全球用户都能使用完整的 Wallet 服务。
供应商可以考虑 Razorpay、Cashfree Payments、PayU、CCAvenue、BillDesk,以及银行直连接口等。最终选择要看供应商当前持有的牌照、合同允许的业务类型、跨境能力和 Wallet 合作模式。供应商的支持范围和监管要求都可能变化,上线前应核对 RBI 最新规则,并取得专业合规意见。
为什么单一 payment gateway 不够
这些功能依赖的资金能力并不相同:
| 功能 | 实际需要的能力 |
|---|---|
| Add money | Payment Gateway/Payment Aggregator、银行卡收单、UPI、net banking |
| Cash out | 银行或合规服务商提供的 Payout API,以及允许提现的 Wallet 产品结构 |
| Transfer money | Wallet 核心账本、KYC、风控、交易限额和 PPI 合规 |
| 保存用户余额 | 客户资金隔离、托管或 escrow 安排、对账和监管报告 |
| 全球使用 | 跨境收付款、外汇、受限主体合规筛查、数据及当地牌照合规 |
支付网关返回“付款成功”,只代表一笔充值交易已被受理或结算。它不负责定义用户的 Wallet 余额,也不应成为 Wallet 账本的唯一依据。
尤其不要把用户充值款直接收进公司的普通银行账户,再把数据库中的数字当成可提现余额。这样会带来客户资金隔离、牌照、审计、退款和清算等多方面的风险。
先确定 Wallet 的监管模式
在印度,如果产品允许用户充值、长期持有余额、向其他用户转账并提现到银行账户,通常会涉及 RBI 的 PPI、KYC、AML/CFT、客户资金保护和支付数据要求。只完成最低限度 KYC 的账户,通常不会自然获得无限制的 P2P 和提现能力。
落地前需要确认:
- 由自己的公司申请并运营 PPI,还是接入持牌银行或 PPI 发行方。
- Wallet 是否允许 cash withdrawal 或 transfer back to source/bank account。
- 用户完成哪一级别的 KYC 后才能转账和提现。
- 单笔、每日、每月的余额和交易限额。
- Full-KYC PPI 的互操作安排,包括适用时通过 UPI 接入。
- 客户资金由谁持有,结算账户或 escrow 如何设置。
- 谁负责可疑交易监控、监管报告、投诉和争议处理。
如果团队缺少支付牌照方面的经验,采用 Banking-as-a-Service 或 PPI issuer partnership 往往更现实。合作方提供受监管的 Wallet 账户和资金通道,应用则负责用户体验、业务规则和自身账本。
推荐的系统结构
Mobile App
|
Backend API
|
+-- KYC/PPI Issuer
| +-- Customer onboarding
| +-- Wallet account
| +-- Limits and compliance
|
+-- Payment Gateway
| +-- Cards
| +-- Net banking
| +-- UPI
|
+-- Payout Provider/Bank
| +-- IMPS
| +-- NEFT/RTGS
| +-- UPI payout
|
+-- Internal Ledger
+-- User balances
+-- Pending/available funds
+-- Fees and reversals
+-- Reconciliation
充值成功后,服务端应先验证 webhook,再根据结算状态和风险策略将资金记入 pending 或 available 余额。提现时先冻结余额,等银行通道返回最终结果后再完成扣款;如果提现失败,则释放冻结金额。
用户间转账通常不需要再次经过 payment gateway。它是在合规 Wallet 体系内完成的两笔原子账务操作:借记付款人账户,贷记收款人账户。
一个安全的充值处理示例
下面是与供应商无关的示意代码。实际签名算法、字段名称和状态值必须以所选网关的官方 API 为准。
import crypto from "node:crypto";
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const actualBuffer = Buffer.from(signature, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
return (
actualBuffer.length === expectedBuffer.length &&
crypto.timingSafeEqual(actualBuffer, expectedBuffer)
);
}
async function handlePaymentWebhook(req, res) {
const rawBody = req.rawBody;
const signature = req.headers["x-provider-signature"];
if (!verifyWebhook(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(rawBody.toString("utf8"));
await database.transaction(async (tx) => {
// event.id 必须建立 UNIQUE 约束,防止 webhook 重放导致重复入账。
const inserted = await tx.webhookEvents.insertIfAbsent({
providerEventId: event.id,
eventType: event.type,
payload: event
});
if (!inserted) {
return;
}
if (event.type !== "payment.captured") {
return;
}
const payment = await tx.topups.findByProviderOrderId(
event.data.orderId,
{ forUpdate: true }
);
if (!payment) {
throw new Error("unknown top-up order");
}
if (
payment.amountMinor !== event.data.amountMinor ||
payment.currency !== event.data.currency
) {
throw new Error("payment amount mismatch");
}
await tx.ledger.post([
{
account: "gateway_clearing",
direction: "DEBIT",
amountMinor: payment.amountMinor,
currency: payment.currency
},
{
account: `user_wallet:${payment.userId}`,
direction: "CREDIT",
amountMinor: payment.amountMinor,
currency: payment.currency
}
]);
await tx.topups.markCompleted(payment.id, event.id);
});
return res.status(200).send("ok");
}
金额必须以最小货币单位的整数保存。例如,₹10.50 应保存为 1050 paise,不能使用浮点数:
CREATE TABLE ledger_entries (
id UUID PRIMARY KEY,
transaction_id UUID NOT NULL,
account_id UUID NOT NULL,
direction VARCHAR(6) NOT NULL
CHECK (direction IN ('DEBIT', 'CREDIT')),
amount_minor BIGINT NOT NULL CHECK (amount_minor > 0),
currency CHAR(3) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX uq_provider_event
ON payment_events(provider, provider_event_id);
同一币种下,每笔交易都应满足:
Σ DEBIT = Σ CREDIT
不要直接执行 wallet.balance = wallet.balance + amount 来完成充值。单一余额字段很难正确处理并发、冻结、退款、拒付和审计。
用户间转账的处理原则
P2P 转账需要在同一个数据库事务中锁定相关账户,并同时写入借贷分录:
BEGIN;
-- 对账户按固定顺序加锁,降低死锁概率。
SELECT account_id
FROM wallet_accounts
WHERE account_id IN (:sender_account, :receiver_account)
ORDER BY account_id
FOR UPDATE;
-- 检查发送方 available balance、KYC 状态和交易限额。
INSERT INTO ledger_entries
(id, transaction_id, account_id, direction, amount_minor, currency)
VALUES
(:entry_1, :transaction_id, :sender_account, 'DEBIT', :amount, 'INR'),
(:entry_2, :transaction_id, :receiver_account, 'CREDIT', :amount, 'INR');
COMMIT;
实际实现还应检查:
- 发送方和接收方的 KYC 状态。
- 余额是否充足,以及是否存在被冻结的资金。
- 单笔、累计和频率限制。
- 设备、IP、收款关系和异常行为。
- 是否命中 sanctions、PEP 或 AML 规则。
Idempotency-Key是否已经处理。- 收款账户是否被冻结、关闭或受限。
提现流程
提现不是充值的反向操作,也不能用退款接口代替。
可以采用以下状态机:
CREATED
-> FUNDS_HELD
-> SUBMITTED
-> PROCESSING
-> SUCCEEDED
失败路径:
SUBMITTED/PROCESSING
-> FAILED
-> FUNDS_RELEASED
基本流程如下:
- 验证用户是否已完成提现所需的 KYC。
- 验证银行账户归属,必要时进行 penny drop 或其他账户验证。
- 检查余额、限额、风险和 beneficiary cooling period。
- 原子地将资金从
available转为held。 - 使用唯一的
Idempotency-Key调用 Payout API。 - 同时处理同步响应、异步 webhook 和主动状态查询。
- 成功后确认扣款,失败后释放冻结资金。
- 每日核对内部账本、网关报表、银行流水和发行方账目。
请求超时时,不能立即认定提现失败。银行可能已经受理请求,只是响应丢失。此时直接重试可能造成重复付款,应使用相同的幂等键查询原交易状态。
如何选择供应商
交易费率只是比较因素之一。至少需要向候选供应商确认以下事项:
业务和牌照
- 是否明确接受 Wallet、PPI、stored value 和 P2P 场景。
- 供应商及其合作银行持有哪些授权。
- 是否提供持牌 PPI issuer 合作,而不只是普通 payment gateway。
- 合同是否允许 Wallet top-up 和用户提现。
- 是否支持所需的 KYC、AML 和交易限额控制。
充值能力
- 支持哪些印度银行卡、net banking、UPI 和国际卡。
- 是否支持 3-D Secure、tokenization 和 recurring payment。
- 国际卡的受理国家、币种、结算币种和额外费率。
- Wallet loading 是否受到更严格的风险限制。
- 如何处理退款、拒付和 fraud reserve。
提现能力
- 是否提供 IMPS、NEFT、RTGS 或 UPI payout。
- beneficiary 验证和新增收款人有哪些限制。
- API 幂等性、webhook 重试和状态查询机制如何设计。
- 银行停机时如何排队、补发和对账。
- 提现失败、退票或账户无效时,资金如何退回。
工程能力
- 是否提供 sandbox,以及 sandbox 与生产环境有哪些差异。
- webhook 是否带签名,密钥能否轮换。
- 是否提供稳定的 transaction ID 和 reconciliation report。
- SLA、限流规则、批量付款和故障通知能力。
- SDK 是否只是对 API 的封装,服务端能否不依赖 SDK 独立接入。
- 是否支持多 payment gateway 路由,避免完全受制于一家供应商。
商务成本
除了 MDR,还需要计算:
- Gateway fee。
- Payout fee。
- GST。
- 国际卡和跨境附加费。
- 货币转换费用。
- 退款和 chargeback 费用。
- PPI、KYC 和账户维护费用。
- reserve、rolling reserve 或最低承诺金额。
- 结算周期产生的资金成本。
“全球用户都能使用”需要单独规划
印度应用能够接受国际卡,不代表它可以向全球用户提供完整的 Wallet 服务。
跨境场景至少涉及:
- 用户所在国家是否要求电子货币、汇款或 money transmission 牌照。
- 印度的 FEMA、外汇和跨境支付规则。
- 国际卡充值能否进入可提现余额,或用于 P2P 转账。
- 资金能否从印度 Wallet 提现到境外银行账户。
- 支持哪些币种,以及汇率由谁确定。
- PCI DSS、数据本地化和隐私要求。
- sanctions、PEP、AML 和来源国风险。
- 国际卡欺诈、拒付和 card testing 攻击。
- 税务和消费者争议处理责任。
第一阶段更容易控制的范围是印度用户、INR、印度 KYC 身份和印度银行账户。系统稳定并确认跨境法律结构后,再按国家或地区逐步开放,不宜一开始就宣称全球可用。
实际落地顺序
可以按以下顺序推进:
- 明确 Wallet 类型、目标用户、币种和资金流。
- 请印度支付领域的律师或合规顾问确认 PPI、KYC、FEMA 和跨境要求。
- 决定自行申请牌照,还是与银行或 PPI issuer 合作。
- 根据业务许可,筛选两到三家 payment gateway 和 payout provider。
- 设计双重记账、余额冻结、幂等、退款、拒付和对账机制。
- 先上线 INR、印度用户和 Full-KYC 场景。
- 测试资金流、并发、重复 webhook、银行超时和日终对账。
- 取得供应商、合作银行和合规团队的书面确认后,再开放国际卡或跨境功能。
判断方案是否合适,不能只看哪家网关提供的接口最多。更重要的是,这套组合能否合法持有客户资金、可靠完成收付,并让每一笔余额变化都可追踪、可对账、可审计。
备注:内容仅供参考。
