> ## Documentation Index
> Fetch the complete documentation index at: https://waffo.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 小程序支付

> 如何在钱包小程序内收款：必须集成哪些前端 JS API 和哪些 Waffo 接口、创建订单时发送什么、退款如何工作、如何用 userid/inquiry 识别用户，以及为什么除非额外 scope 获批你只能拿到用户的公开 ID。

一些钱包托管第三方**小程序**——在钱包自己的应用内运行的小型应用。如果你发布了一个，你可以在其中直接收款而不必把用户送到浏览器：你的小程序请求钱包付款，你的后端与 Waffo 的交互和其他任何订单完全一样。

本页涵盖整个集成：支付路径、退款路径，以及独立的用户身份路径。签名、密钥和请求头与其他所有 Waffo API 相同——见[鉴权与签名](/docs/zh/support/auth-signature-faq)。与小程序无关的订单字段见[创建订单字段指南](/docs/zh/support/order-create-field-guide)。

> [!NOTE]
>
> 发布小程序需要在钱包自己的小程序平台上拥有开发者账户，而你在 API 调用中发送的应用标识由 Waffo 分配。在写任何代码之前，请让你的 Waffo 集成联系人启动该流程——这些标识不是自助获取的。

## 我到底要构建什么？

两条独立的轨道。支付是必需的；身份只有第一步是必需的。

**支付轨道**

| 组件 | 位置 | 是否必需 |
| - | - | - |
| `my.tradePay`（钱包 JS API） | 你的小程序前端 | 是 |
| `POST /api/v1/order/create` | 你的后端 | 是 |
| `POST /api/v1/order/inquiry` | 你的后端 | 是 |
| 支付结果通知（Webhook） | 你的后端 | 是 |
| `POST /api/v1/order/refund` | 你的后端 | 仅当你需要退款 |
| `POST /api/v1/refund/inquiry` | 你的后端 | 两者之一 |
| 退款结果通知（Webhook） | 你的后端 | 两者之一 |

**身份轨道**

| 组件 | 位置 | 是否必需 |
| - | - | - |
| `my.getSystemInfo`（钱包 JS API） | 前端 | 是 |
| `my.getAuthCode`（钱包 JS API） | 前端 | 是 |
| `POST /api/v1/userid/inquiry` | 你的后端 | 是 |
| `POST /api/v1/userinfo/inquiry` | 你的后端 | 仅当你需要资料数据 |

两个身份接口都位于 API key 的 **payin** scope 内，因此你用于创建订单的同一个 key 可以调用它们。

## 支付流程端到端是怎么跑的？

1. 用户在你的小程序中点击*支付*。你的前端调用**你自己的**后端，而不是直接调 Waffo——你的 API key 和签名私钥绝不能到达前端。
2. 你的后端带着下文的小程序字段调用 `/api/v1/order/create`，并收到一个支付 URL。
3. 你的前端把该 URL 传给 `my.tradePay`。
4. 钱包运行自己的支付 UI，并向你的前端返回成功或失败。
5. Waffo 把权威结果发送到你的 `notifyUrl`。你的后端据此履约，而不是依据第 4 步。

最后一点正是引发真实事故的地方。`my.tradePay` 返回的值是一个 **UI 结果**：它告诉你用户应用里发生了什么，而不是资金发生了什么。用户把钱包切到后台、失去信号或强制关闭应用，都可能让你在一笔已成功的支付上完全拿不到前端结果。把 Webhook——或 `/api/v1/order/inquiry` 轮询——当作唯一的事实来源，完全按照[确认支付结果](/docs/zh/support/payment-results-faq)中的描述。

## 创建订单时我要发什么？

普通订单需要的一切，加上三项小程序特有内容：

| 字段 | 值 |
| - | - |
| `paymentInfo.productName` | `MINI_PROGRAM_PAYMENT` |
| `paymentInfo.payMethodName` | 钱包的支付方式名称，与你支付方式配置中显示的一致 |
| `paymentInfo.payMethodProperties.appId` | Waffo 分配给你的小程序应用标识 |
| `userInfo.userTerminal` | `IN_MINI_PROGRAM` |

`userTerminal` 比看上去更重要：它是 Waffo 判断某种支付方式能否服务这个特定请求的依据之一。以 `WEB` 提交的小程序订单可能因没有可用支付方式而被拒绝，因为并非每种方式都支持每种终端。其他接受的值为 `WEB`、`APP` 和 `IN_WALLET_APP`（用户在钱包应用内但不在小程序内）。

如果你不确定你的账户配置了哪个支付方式名称和哪个币种，从 `/api/v1/paymethodconfig/inquiry` 回读——见[支付方式配置与订单限制](/docs/zh/support/payment-method-config-and-limits-faq)。

## 如何给小程序支付退款？

通过普通的退款 API——`/api/v1/order/refund`，并用 `/api/v1/refund/inquiry` 或退款 Webhook 跟踪结果。没有小程序专用的退款接口，也没有小程序专用的订单可退次数规则。

有差异的是**支付方式**的退款能力：是否允许部分退款，以及能否重复。请从支付方式配置读取 `refundAllowed` 而不是假设，并参阅[支付方式退款限制](/docs/zh/support/payment-method-refund-limits-faq)了解每项能力的实际含义。

## 如何在小程序内识别用户？

你的小程序自己无法看到用户是谁；由钱包居中协调。

1. 前端带着你需要的 scope 调用 `my.getAuthCode`。它返回一个短期有效的 `authCode`。
2. 前端把该 `authCode` 发到你的后端。
3. 你的后端带着 `merchantId`、`appId`、`authCode` 和 `appPlatform`（Waffo 分配给你的钱包平台代码）调用 `/api/v1/userid/inquiry`。
4. 你会得到：

| 字段 | 说明 |
| - | - |
| `userId` | 用户在该钱包上的**公开标识**——对你小程序中的该用户稳定 |
| `accessToken` | 仅当你接下来要请求资料数据时需要 |
| `accessTokenExpiryTime` | ISO 8601。token 不是无限期的；不要缓存超过此时间 |

把 `userId` 用作你自己账户关联的键。**不要**把它当作手机号、邮箱地址或跨钱包身份——它是一个限定于你小程序范围的不透明标识。

## 我能拿到用户的邮箱地址来发货吗？

默认不能，这是这项集成中最常见的意外。

`my.getAuthCode` **必须**包含 scope `MINI_PROGRAM` 和 `PUBLIC_ID`。这两个只能让你拿到公开用户标识，别无其他。其余一切都是**必须由钱包平台按商户审批的可选 scope**：

| 可选 scope | 解锁内容 |
| - | - |
| `USER_CONTACTINFO_EMAIL` | 邮箱地址 |
| `USER_CONTACTINFO` | 联系方式 |
| `USER_ADDRESS` | 地址 |
| `USER_BIRTHDAY` | 出生日期 |
| `USER_GENDER` | 性别 |
| `USER_LOGIN_ID` | 用户在钱包上的登录标识 |
| `USER_NAME` | 姓名 |

默认情况下**除公开 ID 之外不授予任何个人信息**。要获得上述任何一项，你必须申请具体的 scope，并书面说明为什么你的小程序没有它就无法运作。审批是钱包平台的决定，不是 Waffo 的，并且是逐个 scope 授予的。

请据此设计：让流程在用户只有公开 ID 的情况下仍能完成购买并收到所购内容。实际上这意味着在你自己的小程序 UI 中收集收货邮箱，并把钱包授予你的任何资料字段当作预填表单的便利——永远不要当作必填输入。

## 资料查询返回什么？

`POST /api/v1/userinfo/inquiry` 接收 `merchantId`、`appId`、`appPlatform` 以及身份步骤得到的 `accessToken`，并返回你已获批 scope 覆盖的内容：

| 字段 | 说明 |
| - | - |
| `userName` | 全名 |
| `nickName` | 显示名 |
| `gender` | 单字母代码（`M` / `F`） |
| `birthday` | 格式化的日期字符串——防御性解析，不要假设分隔符 |
| `contactEmail` | 邮箱地址 |
| `contactMobileNumber` | 手机号，含国家代码 |
| `addressInfo[]` | `address`、`city`、`region`、`postcode`、`addressCountryCode` |
| `userLoginId` | 用户在钱包上的登录标识 |
| `kyc` | 用户的认证等级，两位数代码：未认证、部分认证或完全认证 |

以上每一项都是**条件性的**。未授予你的字段根本不会到达，已授予你的字段也可能因为用户从未填写而缺失。请把映射写成缺失字段是正常情况而不是错误。

## 身份错误码是什么意思？

| 错误码 | 含义 | 怎么做 |
| - | - | - |
| `A0001` | Invalid API secret key | key 错误或环境错误。 |
| `A0002` | Invalid signature | 见签名 FAQ。 |
| `A0003` | Parameter validation failed | 详情会指出字段。body 中的商户 ID 与 API key 的商户 ID 不匹配也落在这里。 |
| `A0006` | Merchant status abnormal | 账户级问题；联系支持。 |
| `A0010` | Operation not allowed in merchant contract | 你的账户未配置此项。 |
| `B0008` | Auth code is invalid | 从 `my.getAuthCode` 获取新码。 |
| `B0009` | Auth code is already expired | 码是短期有效的——立即交换，绝不存储。 |
| `B0010` | Auth code has been used | 每个码只能用一次。不要用同一个码重试交换。 |
| `B0011` | Access token is invalid | 从 `my.getAuthCode` 重新执行身份步骤。 |
| `B0012` | Access token is expired | 重新执行身份步骤；遵守 `accessTokenExpiryTime`。 |
| `C0001` | System error | 重试。 |

`B0008`–`B0010` 这三个码都是"你的 auth code 不再可用"。对每一个的正确处理都一样：回到前端拿新码，而不是重试后端调用。

支付错误是普通的那些——见[常见错误排查](/docs/zh/support/errors-troubleshooting-guide)。

## 我能在小程序内做订阅吗？

可以，作为一个独立产品：`MINI_PROGRAM_SUBSCRIPTION` 而不是 `MINI_PROGRAM_PAYMENT`。它多一项要求——创建订阅时必须提供通过上述身份流程获得的**用户 access token**，因为钱包需要知道是它的哪个用户在授权周期性扣款。

订阅的其他一切不变，包括金额规则、通知和取消。从[订阅 FAQ](/docs/zh/support/subscription-faq)开始，并在向用户承诺功能之前检查你的支付方式支持什么。

## 与普通的托管收银台集成有什么不同？

| | 托管收银台 | 小程序 |
| - | - | - |
| 用户在哪里支付 | 浏览器中的 Waffo 收银台页面 | 钱包应用内，由 `my.tradePay` 驱动 |
| 谁渲染支付 UI | Waffo | 钱包 |
| 如何得知结果 | 跳转（表面）+ Webhook（权威） | JS 回调（表面）+ Webhook（权威） |
| 用户是谁 | 你收集的任何信息 | 钱包的公开用户 ID，加已获批的 scope |
| 收银台客制化 | Portal 设置生效 | 不适用——没有 Waffo 页面 |

重要的后果：**收银台页面的设置一概不适用**。显示语言、方式排序、品牌和结果页都在钱包的 UI 中，所以你的小程序掌控体验。[收银台展示规则](/docs/zh/support/cashier-display-rules-faq)和收银台客制化页面中的一切都与此路径无关。

## 我还能拿到同样的报表吗？

能。小程序订单像其他任何订单一样出现在 Portal 交易列表、导出和结算中，并可通过其产品区分。订单标识的行为完全一致，因此你的对账不需要特殊处理——见[结算报表与导出](/docs/zh/support/settlement-reports-faq)。

## 有小程序用的 Waffo SDK 或 JS 包吗？

没有。小程序端没有任何需要你安装的东西。集成的两半是：

| 端 | 你使用的 |
| - | - |
| 你的小程序前端 | **钱包自己的** JS API——`my.getSystemInfo`、`my.getAuthCode`、`my.tradePay`。它们来自钱包运行时，不是 Waffo。 |
| 你的后端 | Waffo 的普通 REST 接口，按普通方式签名 |

Waffo 发布的 SDK 是调用 REST API 的**服务端**辅助库；它们不是小程序包，也没有可嵌入的前端 bundle。所以"我该把哪个 Waffo 库导入小程序"没有答案——正确答案是没有，你的前端永远不直接与 Waffo 通信。

## `appPlatform` 是干什么的，不传会怎样？

`appPlatform` 告诉 Waffo **auth code 来自哪个钱包平台**。它是两个身份接口已记录的请求契约的一部分，接受的值由 Waffo 随你的 `appId` 一起分配。

始终发送它。当你的账户在多个钱包平台上设置时，它是消除交换歧义的字段，省略它不是可以依赖的做法——没有定义的回退行为供你依赖，而把该值与你已持有的 `appId` 一起存储不花任何成本。

## `B0013` 是什么意思，与 `B0011`、`B0012` 有何不同？

| 错误码 | 含义 |
| - | - |
| `B0011` | Access token is invalid——该值不是 Waffo 能解析的 token |
| `B0012` | Access token is expired——它曾有效，但生命周期已过 |
| `B0013` | Access token is not exist——此请求没有记录在案的 token |

对你的集成来说，三者归结为同一种恢复方式：\*\*回到前端，调用 `my.getAuthCode` 拿新码，换取新 token。\*\*用同一个 token 重试后端调用对它们中任何一个都不可能成功。

不要为这三个建立单独的分支。建一条"重新执行身份流程"的路径，把 `B0011`、`B0012` 和 `B0013` 都路由进去，就像你对 auth code 的 `B0008`–`B0010` 那样。

## token 还没到 `accessTokenExpiryTime`，但创建订阅时提示 token 已过期

因为在扣款那一刻，判断 token 是否仍然有效的权威是**钱包**，不是 Waffo。`accessTokenExpiryTime` 是 token 可能有效的最晚时间——不是它仍然有效的保证。当客户撤销授权、登出，或钱包自行使会话失效时，token 可能提前失效。

发生这种情况时，钱包的裁定以 `B0009`（auth code already expired）或 `B0008`（auth code invalid）的形式在创建调用上到达你，而不是来自身份接口。两者都被归类为**不可重试**：用相同凭据的相同调用会一直失败。

由此得出的设计规则：\*\*在创建订单或订阅的流程中获取新 token，而不是复用存储的 token。\*\*缓存 token 并把 `accessTokenExpiryTime` 当作有效性承诺，是这个错误最常见的单一原因。

## 我能在小程序里做订阅吗，具体要发什么？

可以，作为 `MINI_PROGRAM_SUBSCRIPTION` 产品。在普通订阅字段之外，你要发送客户的 access token：

| 字段 | 值 |
| - | - |
| `paymentInfo.productName` | `MINI_PROGRAM_SUBSCRIPTION` |
| `paymentInfo.payMethodUserAccessToken` | 来自 `/api/v1/userid/inquiry` 的 `accessToken` |

该 token 标识钱包的哪个用户在授权周期性委托，以便钱包把委托绑定到正确的账户。同一字段也存在于 `/api/v1/subscription/change` 上，因此小程序中的套餐变更也需要新获取的 token。

订阅的其他一切不变——周期、试用、金额、状态、重试、通知和取消的行为都与[订阅创建与校验](/docs/zh/support/subscription-creation-validation-faq)及其链接页面上完全一致。

## 之后每次续订都需要新 token 吗？

不需要。token 只在**签约时提供一次**，用于建立委托。之后各期扣款依据的是存储的委托，所以你不需要在每次续订时重新执行身份流程——续订是没有客户在场、不涉及前端的后台扣款。

当客户在你面前授权新东西时你需要新 token：创建订阅，或变更套餐。第 2 期及之后不需要。

如果续订因客户在钱包中撤销了委托而失败，它以支付方式一侧的取消形式到达，而不是 token 错误——见[订阅 FAQ](/docs/zh/support/subscription-faq)中的钱包撤销行为。

## 支付后我能把客户送回小程序中的特定页面吗？

那是你前端的工作，不是 Waffo 的。因为这个流程中任何地方都没有 Waffo 托管的页面，创建订单时的跳转 URL 在这里没有作用对象——导航留在钱包内，钱包的支付 UI 关闭时控制权返回到调用 `my.tradePay` 的页面。

所以：在你离开的页面上恢复，并根据**你后端**对订单的视图（由 Webhook 驱动）决定显示什么。不要纯凭 JS 回调渲染成功页面，原因见[确认支付结果](/docs/zh/support/payment-results-faq)。

## 在 Portal 的哪里能看到一笔支付来自哪个小程序？

在**单笔支付的详情页**上，对于带有小程序的订单，它有一个小程序区块，显示 **App ID** 和 **App Name**。从 **Transactions → Payments** 打开该支付，区块就在订单其他信息旁边。

**不**提供的是：支付**列表**上没有小程序筛选或 App ID 列，因此你无法按小程序切分列表。如果你运营多个小程序并需要按应用报表，请以你自己的订单记录为键——你知道每次 `/api/v1/order/create` 发的是哪个 `appId`——而不是期望 Portal 替你分组。

小程序产品在交易导出中有名称，所以流量可识别为小程序流量；只有*哪个*小程序是仅详情页可见的。

## 这个客户和我网页收银台上的是同一个人吗？

不会自动是，你也不应试图让 Waffo 回答这个问题。你拿到的 `userId` 是钱包的**限定于你小程序范围的公开标识**——它不是邮箱地址、不是手机号，也不是跨渠道身份。

这意味着：

* 你无法通过比较标识把小程序购物者与网页收银台购物者匹配起来。没有共享的东西可比。
* 如果你发布两个小程序，不要假设同一个人在两者中的标识相同。按不同来设计。
* 可行的模式是你自己的账户系统：客户在小程序内登录你的账户，你把钱包 `userId` 存到该账户上一次，从此你的账户 ID 就是跨渠道的身份。

在你已获授联系 scope 的情况下，邮箱地址可以帮你向客户*提议*一个匹配——但把它当作需要与客户确认的提示，绝不要当作两条记录是同一个人的证明。

## 接下来看哪里？

* [确认支付结果](/docs/zh/support/payment-results-faq)——为什么 Webhook 而不是 JS 回调才是你的事实来源。
* [创建订单字段指南](/docs/zh/support/order-create-field-guide)——创建订单契约的其余部分。
* [支付方式配置与订单限制](/docs/zh/support/payment-method-config-and-limits-faq)——回读你拥有哪些方式、币种和金额范围。

卡在应用标识或 scope 审批上？请[联系支持](mailto:support@waffo.com)——两者都是开通步骤，不是代码问题。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.