如何为网站集成支持 API 的 SMS payment solution?
结论
可以集成,但通常没有一套覆盖所有国家和运营商的通用 SMS 支付 API。实际项目一般需要接入当地运营商认可的 Premium SMS(高级资费短信)或 Direct Carrier Billing(运营商代扣)聚合商。
用户可以在网站内输入手机号、确认验证码并解锁内容,但并非所有运营商都支持全程留在网站内。有些流程要求用户发送短信、打开运营商确认页,或完成额外的身份验证。接入前,需要向服务商确认是否提供嵌入式流程、服务端 API、异步回调,以及是否覆盖目标国家的运营商。
推荐的支付流程
对于 1、3、5 或 10 欧元的小额内容,常见的支付方式有两种。
Premium SMS
用户向指定短号码发送关键词,例如:
发送 ACCESS123 到 12345
运营商收取短信费用,支付平台再通过 Webhook 通知网站。网站验证通知后,为用户开放付费内容。
这种方式实现起来比较简单,但用户必须离开浏览器发送短信。不同价格通常还需要使用不同的短号码或关键词。
Direct Carrier Billing
用户在网站输入手机号,服务商向该号码发送一次性验证码。用户在当前页面提交验证码后,费用会计入手机账单,或从预付费余额中扣除。
典型流程如下:
- 用户选择价格档位。
- 服务器创建支付订单。
- 用户输入手机号。
- 支付平台发送验证码,或触发运营商确认。
- 用户在网站输入验证码。
- 支付平台处理扣款。
- 支付平台通过 Webhook 返回最终结果。
- 服务器确认支付成功并开放内容。
如果希望用户尽量留在网站内,应优先选择支持 API、OTP/PIN 验证和嵌入式结账页面的 Direct Carrier Billing 服务商。
服务端集成示例
各家服务商的接口名称和参数并不统一。下面只展示一个通用结构,其中的 URL、字段名和签名算法都是占位内容,接入时必须替换为服务商官方文档规定的值。
创建订单
不要让浏览器直接决定最终金额。服务器应根据内部商品编号确定价格:
const PRODUCTS = {
article_1: { amount: 100, currency: "EUR" },
article_3: { amount: 300, currency: "EUR" },
article_5: { amount: 500, currency: "EUR" },
article_10: { amount: 1000, currency: "EUR" }
};
app.post("/api/sms-payments", async (req, res) => {
const product = PRODUCTS[req.body.productId];
if (!product) {
return res.status(400).json({ error: "Unknown product" });
}
const order = await db.orders.create({
userId: req.user.id,
productId: req.body.productId,
amount: product.amount,
currency: product.currency,
status: "pending"
});
const response = await fetch("https://api.provider.example/payments", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SMS_PAYMENT_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": order.id
},
body: JSON.stringify({
merchantReference: order.id,
amount: product.amount,
currency: product.currency,
phoneNumber: req.body.phoneNumber,
callbackUrl: "https://example.com/webhooks/sms-payment"
})
});
if (!response.ok) {
return res.status(502).json({ error: "Payment provider unavailable" });
}
const payment = await response.json();
res.json({
orderId: order.id,
paymentId: payment.id,
nextAction: payment.nextAction
});
});
金额最好使用最小货币单位。例如,100 表示 1.00 EUR,这样可以避免浮点数计算误差。不过,仍要确认服务商要求的是整数分、十进制字符串,还是预先配置的价格档位。
提交验证码
如果服务商支持通过 API 验证 OTP/PIN,可以在网站中提供验证码输入框:
app.post("/api/sms-payments/:paymentId/confirm", async (req, res) => {
const response = await fetch(
`https://api.provider.example/payments/${encodeURIComponent(req.params.paymentId)}/confirm`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SMS_PAYMENT_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
code: req.body.code
})
}
);
const result = await response.json();
res.status(response.ok ? 200 : 400).json({
status: result.status
});
});
支付 API 密钥只能保存在服务器端,不能写入网页 JavaScript、移动端代码或公开仓库。
处理支付回调
不能只根据用户提交验证码后的页面响应开放内容。网络超时、延迟扣款和重复通知都很常见,最终结果应以通过签名验证的 Webhook 为准:
import crypto from "node:crypto";
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
if (
typeof signature !== "string" ||
signature.length !== expected.length
) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(signature, "utf8"),
Buffer.from(expected, "utf8")
);
}
app.post(
"/webhooks/sms-payment",
express.raw({ type: "application/json" }),
async (req, res) => {
const signature = req.get("X-Payment-Signature");
if (
!verifySignature(
req.body,
signature,
process.env.SMS_PAYMENT_WEBHOOK_SECRET
)
) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "payment.succeeded") {
const order = await db.orders.findById(event.merchantReference);
if (
order &&
order.status === "pending" &&
order.amount === event.amount &&
order.currency === event.currency
) {
await db.transaction(async (tx) => {
await tx.orders.markPaid(order.id, event.paymentId);
await tx.entitlements.grant(order.userId, order.productId);
});
}
}
res.sendStatus(204);
}
);
实际签名方式可能涉及原始请求体、时间戳、证书或其他编码格式,必须严格按照服务商的签名规范实现。还要保存事件 ID,并让回调处理保持幂等,避免重复通知导致多次授权。
选择服务商时要确认什么
不能只看服务商有没有 API 文档,还要逐项确认:
- 是否覆盖用户所在国家及其移动运营商;
- 是否支持一次性付款,而不只是周期订阅;
- 能否提供 1、3、5、10 欧元这些价格档位;
- 用户是否必须跳转到运营商页面;
- 是否支持网页内 OTP/PIN 验证;
- 是否提供服务端 API、Webhook、签名验证和沙箱环境;
- 手机号由谁收集、保存和处理;
- 用户账单中显示什么商户或服务名称;
- 退款、拒付、投诉和未成年人消费如何处理;
- 分成比例、最低结算额及结算周期;
- API 返回的是扣款成功,还是仅表示请求已受理。
运营商支付的实际到账金额通常会明显低于用户支付的金额。除了聚合商手续费,还可能扣除运营商分成、税费和退款准备金。因此,标价 1 EUR 不代表商户最终能够收到 1 EUR。
合规和体验方面的限制
SMS 支付受国家、运营商和内容类型影响很大。服务商可能要求提交公司注册资料并接受内容审核,还可能要求页面提供清楚的价格说明、退款入口、客服联系方式和指定的确认文案。成人内容、博彩、数字货币等类别通常会受到更多限制,也可能直接无法接入。
页面至少应清楚说明:
- 单次收费还是自动续费;
- 含税总价;
- 费用将计入手机账单或从余额扣除;
- 内容有效期;
- 退款和客服渠道;
- 用户需要执行的确认操作。
手机号不能作为可靠的登录身份,因为号码可能被回收、转移,也可能由多人共用。内容授权应绑定网站账户和内部订单,手机号只用于支付流程中的身份标识。
系统还应限制验证码尝试次数、订单创建频率和同一手机号的请求频率,并记录订单状态变化,以防止暴力尝试、重复扣款和伪造回调。
可行的落地方案
如果业务只面向一个国家,可以优先寻找当地运营商认可的聚合商。先确认价格档位和嵌入式支付能力,再开始开发。如果业务覆盖多个欧洲国家,则可能需要按国家分别签约,并分别配置短号码或支付流程。
如果没有服务商能够提供完全在网站内完成的流程,可以采用以下方案:
- 在网站内完成商品选择和订单创建;
- 通过弹窗、嵌入式组件或短暂打开的运营商确认页完成授权;
- 用户返回后显示”正在确认支付”;
- 服务器等待 Webhook;
- 收到最终成功通知后自动开放内容。
API 集成本身是可行的,但能否完全避免跳转,不只取决于技术实现,还要看运营商和支付服务商允许使用哪种认证方式。确定目标国家、运营商覆盖范围和内容类型后,再根据结算比例、合规要求和用户流程选择聚合商,比单纯寻找一个”SMS API”更实际。
备注:内容仅供参考。
