整体结构
收集 token、手机号、WhatsApp OTP、PIN。初始化成功后才允许输入 OTP,OTP 成功后才允许输入 PIN,避免用户跳步骤。
提供 /api/checkout/start、/api/gopay/otp、/api/gopay/pin,并负责组装外部请求、重试和错误透传。
config.json 控制 checkout 地址、mock/正式基础地址、Midtrans header、代理测试地址;环境变量可以覆盖配置。
作者与声明
作者微信:cian0318。本文档和工具仅供学习交流,不可用于其他用途。遇到流程问题可扫码添加微信;觉得有帮助可以扫码赞助。
转载请保留作者信息;如果删除作者信息,愿你以后写的代码全是 BUG。
协议流程图
上半部分是用户可见的三步操作;下半部分是 PIN 提交后后端自动执行的支付确认链路。
本地后端接口
| 接口 | 前端发送 | 后端返回 | 用途 |
|---|---|---|---|
POST /api/checkout/start |
访问令牌、GoPay 手机号、国家码、代理、税务地区;邮箱由前端随机生成 Gmail。 | reference_id、gopay_guid、next_action: enter_otp、Stripe/Midtrans/GoPay 阶段响应。 |
创建 checkout session,初始化 Stripe,打开 Midtrans GoPay linking,并停在 OTP 前。 |
POST /api/gopay/otp |
reference_id 和用户输入的 otp。 |
challenge_id、next_action: enter_pin、完整 gopay_otp。 |
验证 WhatsApp OTP,拿到后续 PIN 验证需要的 challenge。 |
POST /api/gopay/pin |
reference_id、challenge_id、gopay_guid 和用户输入的 pin。 |
ok: true、stage: gopay_complete、midtrans_status、各阶段原始响应。 |
完成绑定 PIN 验证、发起 charge、完成支付 PIN 验证并查询最终状态。 |
初始化账单阶段访问了什么
Checkout 与 Stripe 初始化
后端先把前端参数转发到配置里的 checkout 接口,取得 checkout_session_id 和 publishable_key。随后调用 Stripe 页面初始化、更新账单信息、创建 gopay 类型 payment method,再确认 session。
审批与跳转信息
后端调用 checkout approve 接口,随后读取 Stripe payment details,检查 setup_intent.next_action 或 payment_intent.next_action 中的跳转地址。
Midtrans GoPay linking
后端跟随 GoPay redirect 得到 gopay_guid,再请求 POST /snap/v3/accounts/{gopay_guid}/linking,发送手机号数据并从 activation_link_url 提取 reference_id。
{
"type": "gopay",
"country_code": "86",
"phone_number": "1888888888"
}
Reference 与用户授权检查
后端继续调用 POST /v1/linking/validate-reference 和 POST /v1/linking/user-consent,都使用 reference_id。成功后返回给前端,按钮进入 OTP 阶段。
OTP 与 PIN 阶段访问了什么
本地 /api/gopay/otp 调用 GoPay POST /v1/linking/validate-otp。请求体只包含 reference 和 OTP。当前实现只覆盖 WhatsApp 验证码,不包含 SMS 发送接口;如果需要短信通道,需要自行补充发送 SMS 的接口和前端入口。
{
"reference_id": "前一步返回的 reference_id",
"otp": "用户输入的 OTP"
}
本地 /api/gopay/pin 先调用 POST /api/v1/users/pin/tokens/nb,把 OTP 阶段返回的 challenge 和 PIN 换成 pin_token。
{
"challenge_id": "OTP 返回的 challenge_id",
"client_id": "51b5f09a-3813-11ee-be56-0242ac120002-MGUPA",
"pin": "用户输入的 PIN"
}
PIN 后的支付完成链路
| 顺序 | 外部接口 | 发送/提取的数据 | 成功条件 |
|---|---|---|---|
| A | POST /v1/linking/validate-pin |
发送 reference_id 和上一步的 pin_token。 |
success: true,并进入 linking 成功状态。 |
| B | POST /snap/v2/transactions/{gopay_guid}/charge |
发送 payment_type: gopay、tokenization: true、promo_details: null。 |
响应里必须有 gopay_verification_link_url,后端从中提取新的支付 reference_id。 |
| C | GET /v1/payment/validate?reference_id=... |
查询 GoPay 支付页数据,包括商户、金额、支付工具 token。 | success: true 且 next_action: payment-details。 |
| D | POST /v1/payment/confirm?reference_id=... |
发送 {"payment_instructions":[]}。 |
返回 payment-validate-pin,并给出支付 PIN 的 challenge_id 和 client_id。 |
| E | POST /api/v1/users/pin/tokens/nb |
发送支付 confirm 返回的 challenge_id、client_id 和同一个 PIN。 |
返回新的 data.token。 |
| F | POST /v1/payment/process?reference_id=... |
发送 GOPAY_PIN_CHALLENGE 和支付 PIN token。 |
success: true 且 next_action: payment-success。 |
| G | GET /snap/v1/transactions/{transaction_id}/status |
从 process 的 redirect_url 提取交易 id,再查询 Midtrans 状态。 |
页面展示 transaction_status,例如 settlement。 |
关键字段如何流转
| 字段 | 来源 | 后续用途 |
|---|---|---|
gopay_guid |
Stripe next_action 跳转到 Midtrans 后,由后端解析 redirect 得到。 | 用于 Midtrans linking 和 charge 的 URL 路径。 |
reference_id |
Midtrans linking 返回的 activation_link_url。 |
用于 reference 校验、用户授权、OTP、绑定 PIN。 |
challenge_id |
GoPay OTP 返回的 data.challenge.action.value.challenge_id。 |
用于换取绑定阶段 PIN token。 |
payment_reference_id |
Midtrans charge 返回的 gopay_verification_link_url。 |
用于 payment validate、confirm、process。 |
midtrans_status |
最后的 Midtrans transaction status 接口。 | 作为页面最终结果展示,并保留完整 JSON 方便排查。 |
配置与 mock
checkout_mode 可设置为 upstream 或 local。local 会跳过 ChatGPT checkout 初始化并使用本地模拟流程(适合排查联调);upstream 走真实上游。其余 local_mock_base_url、midtrans_mock_base_url、gopay_gwa_mock_base_url、gopay_customer_mock_base_url 决定后端访问哪里。默认可指向 http://localhost:8282 进行本地 mock;替换成正式域名会访问真实外部链路。
错误如何定位
每个阶段失败时,后端都会返回 stage 和对应阶段的原始响应,例如 gopay_payment_confirm、gopay_payment_process、midtrans_status。排查时先看 stage,再看该阶段对象里的 endpoint、status、status_message 或 GoPay errors。