> ## 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.

# 创建订单字段指南

> 完整的 /api/v1/order/create 请求字段表及每个长度限制、哪些字段必填、值过长时会怎样、重发时 paymentRequestId 幂等检查的实际行为，以及常见校验错误背后的字段规则。

本页说明商户最常问到的创建订单字段、带长度限制的完整字段表，
以及最常见错误背后的校验规则。

## 三个 ID，各在哪里用

| 字段 | 谁生成 | 用途 |
| - | - | - |
| `paymentRequestId` | **您**（String 32） | 标识本次创建**请求**，用于幂等检查。复用同一值可安全重试同一订单；不同订单必须使用新值。 |
| `merchantOrderId` | **您**（String 64） | 您的业务订单号，原样传递供您自己参考和对账。 |
| `acquiringOrderId` | **Waffo**（String 32） | Waffo 的订单 ID，在创建响应中返回。用于查询和退款等后续调用，以及匹配Webhook 通知。 |

**幂等的实际表现：**用相同 `paymentRequestId` 和
完全相同的参数重试会返回同一订单，而不是创建重复订单。
用相同 `paymentRequestId` 但**不同**参数重试
会被以 `A0011 Idempotent param mismatch error` 拒绝。

## 有一张表能看到哪些字段必填、最长多少吗？

有——就是这张。这里的"必填"指*对每一笔订单都必填*。第二组
更大的字段在契约中是可选的，但在特定支付方式或场景下
变为必填；这些单独列在
[各支付方式的条件必填字段](/docs/zh/support/conditional-required-fields-faq)，
查找表在 [API 参考表](/docs/zh/support/api-reference-tables)。

**顶层**

| 字段 | 必填 | 最大长度 |
| - | - | - |
| `paymentRequestId` | 是 | 32 |
| `merchantOrderId` | 是 | 64 |
| `orderCurrency` | 是 | 12 |
| `orderAmount` | 是 | 24 |
| `orderDescription` | 是 | 128 |
| `orderRequestedAt` | 是 | —（时间戳） |
| `notifyUrl` | 是 | 1024 |
| `orderExpiredAt` | 否 | —（时间戳） |
| `userCurrency` | 否 | 12 |
| `successRedirectUrl` | 否 | 1024 |
| `failedRedirectUrl` | 否 | 1024 |
| `cancelRedirectUrl` | 否 | 512 |
| `extendInfo` | 否 | 128 |
| `paymentTokenData` | 否 | 8192 |
| `promotionInfo.promotionCode` / `.couponId` | 否 | 各 64 |

**`merchantInfo`**（对象本身必填）

| 字段 | 必填 | 最大长度 |
| - | - | - |
| `merchantId` | 是 | 64 |
| `subMerchantId` | 否 | 64 |

**`userInfo`**（对象本身必填）

| 字段 | 必填 | 最大长度 |
| - | - | - |
| `userId` | 是 | 64 |
| `userEmail` | 是 | 64 |
| `userTerminal` | 是 | 32 |
| `userPhone` | 否 | 30 |
| `userCountryCode` | 否 | 3 |
| `userFirstName` / `userLastName` | 否 | 各 64 |
| `userCreatedAt` | 否 | —（时间戳） |
| `userBrowserIp` | 否 | 128 |
| `userAgent` | 否 | 256 |

**`paymentInfo`**（对象本身必填）

| 字段 | 必填 | 最大长度 |
| - | - | - |
| `productName` | 是 | 32 |
| `payMethodType` | 否 | 256 |
| `payMethodName` | 否 | 256 |
| `payMethodProperties` | 否 | 1024 |
| `payMethodCountry` | 否 | 3 |
| `payOption` | 否 | — |
| `userPaymentAccessToken` | 否 | 256 |
| `payMethodUserAccountNo` | 否 | 64 |
| `payMethodUserAccountType` | 否 | 24 |
| `cashierLanguage` | 否 | 24 |
| `cashierAppearance` | 否 | —（JSON 字符串） |
| `receiptTemplateId` | 否 | 32 |
| `captureMode` | 否 | 24 |
| `merchantInitiatedMode` | 否 | 24 |

**`goodsInfo`**——对象可选。契约将 `goodsName`
和 `goodsUrl` / `appName` 之一记为预期内容（用于合规与风控
审核），但创建请求只强制执行下面的长度限制；某支付方式
是否坚持要求它们，是按方式的条件规则

| 字段 | 最大长度 |
| - | - |
| `goodsName` | 64 |
| `goodsUrl` | 128 |
| `appName` | 32 |
| `goodsId` | 32 |
| `skuName` | 32 |
| `goodsCategory` | 32 |
| `goodsUniquePrice` | 16 |
| `goodsQuantity` | —（整数） |

**`cardInfo`**、**`riskData`**、**`addressInfo`**、**`brandInfo`**——均为可选对象

| 字段 | 最大长度 |
| - | - |
| `cardInfo.cardNumber` | 24 |
| `cardInfo.cardCvv` | 4 |
| `cardInfo.cardHolderName` | 128 |
| `cardInfo.threeDsDecision` | 12 |
| `cardInfo.cardExpiryYear` / `cardExpiryMonth` | —（整数） |
| `riskData.userType` / `userCategory` | 各 24 |
| `riskData.userLegalName` / `userDisplayName` | 各 128 |
| `riskData.userRegistrationIp` / `userLastSeenIp` | 各 24 |
| `riskData.userIsNew` / `userIsFirstPurchase` | 各 8 |
| `addressInfo.address` | 256 |
| `addressInfo.city` / `region` | 各 32 |
| `addressInfo.postcode` | 16 |
| `addressInfo.addressCountryCode` | 16 |
| `brandInfo.cashierLogoUrl` / `cashierProductImageUrl` | 各 512 |
| `brandInfo.cashierDisplayName` | 128 |

> [!NOTE]
>
> 注意这个容易让人中招的不对称：`successRedirectUrl` 和
> `failedRedirectUrl` 允许 **1024**，`cancelRedirectUrl` 只允许 **512**。一个
> 在其中两个字段上通过的 URL 可能在第三个字段上失败。

## 值太长会被截断还是拒绝？

\*\*拒绝。\*\*长度限制在请求上强制执行，在任何订单创建
之前；没有任何东西会被静默截断，也不存在
超长值被以缩短形式接受的字段。失败以
`A0003 Parameter validation failed` 返回，信息中点名出问题的字段，
所以请读信息，而不是手工比对您的报文。

实际影响：调用前在您这边校验长度，尤其是
您通过拼接构造的字段（`orderDescription`、`merchantOrderId`、
`goodsName`）以及任何用户输入的内容。

## `extendInfo` 只有 128 个字符，能传更多吗？会返回给我吗？

会返回给您，但不能变大。

* **会返回给您**：`extendInfo` 原样存储，并在
  `/api/v1/order/inquiry` **和**支付通知中回显——两者携带
  相同的订单结构，所以您放进去的内容在两处都原样返回。
* **128 是上限**，且是请求校验上限，所以 129 个字符
  是 `A0003`，不是截断。

所以请把它当作**键，而不是载荷**：放入您自己的订单引用或一个短的
路由令牌，其余在您自己的系统中查找。`merchantOrderId`
（64）是为您的业务订单号设计的字段；两者结合
足以作为任何查找的键。如果您需要在 Waffo 侧存放按订单的结构化数据，
携带语义的字段——`goodsInfo`、`riskData`、
`addressInfo`——才是预期的归属，而不是 `extendInfo`。

## 重发同一个 `paymentRequestId` 到底会发生什么？

`paymentRequestId` 的作用域是您的商户 ID，检查在任何
新订单创建之前运行。会做两项比较——`orderAmount` 和
`orderCurrency`：

| 您重发的内容 | 发生什么 |
| - | - |
| 相同 id，相同金额和币种 | 您拿回**现有**订单，而不是新订单 |
| 相同 id，**不同**金额或币种 | 以 `A0011 Idempotent param mismatch error` 拒绝 |
| 相同 id，与第一次调用同一时刻 | 一次调用胜出；另一次得到 `A0040 Order cannot be concurrently created` |

返回现有订单时，您收到的内容取决于该订单
进行到了哪里：仍在处理中的订单会重试其支付步骤，您
收到当前的支付详情；已到达最终状态的订单
重放存储的结果——包括重放原始失败（如果它
失败了）。这是有意的：重试永远不能把已定的订单变成
不同的结果。

两个值得据此设计的后果：

* \*\*只比较金额和币种。\*\*用相同 id 重发但
  支付方式、用户或描述不同，**不会**
  触发不匹配——它返回第一个订单。如果您想要一个真正
  不同的订单，请使用新的 `paymentRequestId`。
* \*\*`A0040` 是"再试一次"，不是失败。\*\*它表示您的两次调用撞车了。
  不要因此创建第二个订单 id；用相同 id 重新调用，或对它
  运行订单查询。

## 我收到了 `A0040 Order cannot be concurrently created`，应该重试吗？

是——用**相同**的 `paymentRequestId`。它在同一 id 的两次创建
同时在途时抛出，所以其中恰好一次在做实际工作，另一次
被挡回以保护您免于重复订单。短暂停顿后重试，
或直接对该 id 运行订单查询；无论哪种方式，您都会收敛
到那唯一存在的订单上。

您绝不能做的是把它当作"订单未创建"并用
新 id 重新发起。真正的重复扣款就是这样产生的。

## 我能把 `orderAmount` 传 0 吗？

零金额不会被通用金额检查拒绝——请求级规则
是金额不能为**负**——但零金额订单能否
实际创建取决于您使用的支付产品和支付方式，
而且零金额对其中大多数没有意义。

所以不要在此基础上构建流程。如果您的用例是"验卡不扣款"，
为此设计的机制是
[绑卡](/docs/zh/support/tokenization-faq)，不是零金额订单。如果您的
用例是免费订单，就完全不要创建 Waffo 订单。

负金额以 `A0003` 失败。

## 我能传几位小数？金额会因形状被拒吗？

小数位数按该币种的 ISO 4217 精度检查，
小数过多的值以 `A0003` 失败——完整规则，包括
必须不带小数的币种以及跨币种订单的不同处理，见
[金额与币种](/docs/zh/developer-docs/core-concepts/currency)和
[订单金额、限额与拒绝](/docs/zh/support/order-amount-precision-and-decline-faq)。

还有第二项不太明显的检查。某些支付方式与币种组合
只接受**某个步长的整数倍**金额——例如只接受十的倍数，
或只接受完全不带小数的整数单位——
且该规则绑定在组合上，而不是仅在币种上。这就是为什么
在某币种下完全有效的金额仍可能在某一特定方式上被拒绝。
如果您的价格不是整数，请用真实金额而不是凑整金额测试
您打算提供的每种方式，并把拒绝当作配置事实而不是 bug。

## `orderRequestedAt` 需要什么格式？我的时钟必须和你们一致吗？

格式为 ISO 8601，精确到**毫秒**，以字面 `Z` 结尾——
`2023-04-03T03:00:00.000Z`。它是 UTC 时间戳：`+08:00` 这样的数字偏移
不是可接受的替代，只到秒的精度也不行。
无法解析的值以 `A0003` 失败，错误信息会说明
预期格式。

您的时钟不必与 Waffo 一致——`orderRequestedAt` 记录为
*您的*请求时间，订单不会因与服务器时间有偏差而被拒绝。
**确实**会与时钟比较的时间戳是 `orderExpiredAt`——
且比较是宽容的：已经过去的过期时间在创建时
被**丢弃**，订单以该支付方式的默认过期时间创建，
所以您这边的时钟 bug 不会表现为错误。它
表现为一个开放时间比您预期更长的订单。`A0034 The order
expiry time provided by merchant has expired` 存在于契约中，但您
不会因创建时传入过去的 `orderExpiredAt` 而看到它。用同一种 UTC 格式发送
两个时间戳，这一整类问题就消失了。

## `riskData` 有什么用？不传会对我有影响吗？

`riskData` 携带您已知但 Waffo 看不到的用户信息：
是否是新用户（`userIsNew`）、是否首次购买
（`userIsFirstPurchase`）、个人还是企业（`userType`）、注册时的
IP（`userRegistrationIp`）、最近出现的
IP（`userLastSeenIp`），以及其法定名称和显示名称。

它是可选的。其中任何内容都不会与任何东西比对校验，省略它
不会导致订单失败。它的作用是让风控筛查在一笔
本来只能凭支付本身判断的交易上有更多依据——这正是
这些字段存在的原因。如果您发送它，两条规则：发送**真实**值，并
一致地发送，因为不一致的历史数据比没有历史数据更糟。
不要编造注册 IP，也不要把所有用户都标记为回头客。

同样的逻辑适用于 `userInfo.userBrowserIp` 和 `userInfo.userAgent`：
一般可选，但当您自己收集卡信息并通过 API 提交
卡、Apple Pay 或 Google Pay 数据而不是使用 Waffo
收银台时，它们是预期要传的——在那种流程中它们是真实浏览器的唯一信号。
`addressInfo` 跟随支付方式：契约中可选，
在支付方式或场景要求收货地址时变为必填——
见[条件必填字段](/docs/zh/support/conditional-required-fields-faq)。

## 我必须发送 `goodsInfo` 吗？`goodsUrl` 或 `appName` 够吗？

对象本身是可选的，创建请求**不**强制执行其内部字段，
仅限于长度限制——只带 `goodsName` 的 `goodsInfo`
会被接受。契约将 `goodsName` 加 `goodsUrl` **或**
`appName` **之一**记为预期内容，因为那是合规与风控
审核所查看的：网页商户发送产品 URL；纯 App 商户发送
包名，例如 `com.example.app123`。它们是二选一，
不是两者都要。

当 `goodsInfo` 内的某个字段*确实*因缺失被拒绝时，规则来自
支付方式，而不是该对象：某些方式要求商品
详情，并像其他任何条件必填字段一样报告缺失路径——见
[条件必填字段](/docs/zh/support/conditional-required-fields-faq)。

所以实用建议是：要么省略该对象，要么完整发送。省略
它本身不会让订单被拦截，但它从风控筛查中移除了商品
上下文，而对某些业务类别来说，那正是
让边缘交易获批的上下文。

## 我能限制只用特定卡 BIN 支付吗？

可以，通过 `paymentInfo.payMethodProperties`，它是一个 JSON 字符串。涉及
两个键：

| 键 | 含义 |
| - | - |
| `cardPrefix` | 允许的卡前缀（BIN）。每个 **4 到 9 位数字**，逗号分隔，最多 **80** 个前缀 |
| `cardPrefixPurpose` | 限制原因：`restriction` 或 `promotion`——这决定向用户显示的信息 |

仅限数字，且每个前缀必须在 4 到 9 位范围内；其他任何情况
都是 `A0003`。请有意地设置 `cardPrefixPurpose`：被告知"此
卡不符合本次促销条件"的购物者与被告知
"此卡不被接受"的购物者反应截然不同，而这两种措辞来自这一个值。

同一个 properties 字段也是少数其他方式专属键的所在，
包括某些市场要求的付款人税号——见
[条件必填字段](/docs/zh/support/conditional-required-fields-faq)。

## 我能决定支付是否走 3DS 吗？

您可以在类卡支付上通过
`cardInfo.threeDsDecision` 表达一个决定。它恰好接受两个值：

| 值 | 含义 |
| - | - |
| `3DS_FORCE` | 用 3DS 认证 |
| `NO_3DS` | 不用 3DS 认证 |

其他任何值都会被作为无效卡数据拒绝。在依赖它之前需要理解
两个限制：

* 它仅适用于**客户在场**的**类卡**支付。
  在商户发起的交易上——您对存储 token 发起、无客户
  在会话中的扣款——该字段不生效。
* 它是您表达的决定，不是对发卡行的保证。经过认证的
  支付随后是否成功，以及未认证的支付是否被银行
  拒绝，由发卡行决定。

如果您想在自己的页面上收集卡号，这在成为参数问题之前
首先是合规问题：您需要相应的
卡数据资质，而替代方案——让 Waffo 的收银台或
绑卡流程处理卡——正是
[绑卡](/docs/zh/support/tokenization-faq)和
[收银台](/docs/zh/developer-docs/integration/checkout/steps)集成存在的目的。

## 客户不在场时我怎么对存储的卡扣款？

发送 `paymentInfo.merchantInitiatedMode`，它接受两个值：

| 值 | 用于 |
| - | - |
| `scheduled` | 按已知计划的扣款——续订或分期 |
| `unscheduled` | 由您的业务在不可预测时刻触发的扣款——账户充值、用量超额 |

只要发送 `merchantInitiatedMode`，`userPaymentAccessToken` 就是**必填**的；
没有它请求会校验失败，因为
没有可扣款的存储凭证。发送该模式还会把交易标记为
商户发起，这正是改变其认证方式的原因——也是
`threeDsDecision` 不再生效的原因。

关于 token 最初如何创建，以及客户在场的首次支付与
后续扣款的区别，见
[绑卡](/docs/zh/support/tokenization-faq)。

## `payMethodName` 能带多个值吗？

可以。`payMethodName` 和 `payMethodType` 都接受**逗号分隔的列表**，
且值匹配不区分大小写。这就是把收银台收窄到
一个子集而非恰好一种方式的做法——例如同时传入某卡组织的信用卡
和借记卡条目，让购物者可以用任一种。

常见情形的已发布参数指引值得严格遵循：
对开放的卡受理，传 `payMethodType="CREDITCARD,DEBITCARD"` 并将
`payMethodName` 留空，让卡组织根据购物者输入的 BIN
确定；要固定单一方式，两个字段都传；要让购物者
自由选择，两个都不传。您账户的确切值来自
`/api/v1/paymethodconfig/inquiry` 或 Portal 的 Payin 页面——不要硬编码
从规范复制的列表。

## `merchantInfo.merchantId` 和我的 API key 不匹配会怎样？

立即以 `A0003` 失败，在任何订单处理之前：请求体中的商户
ID 必须是您签名所用 API key 所属的商户。
同一检查有三个相邻的失败情形值得区分：

| 情形 | 错误码 |
| - | - |
| `merchantId` 与 API key 不匹配 | `A0003` |
| `merchantId` 根本不存在 | `A0003` |
| `subMerchantId` 在该商户下不存在 | `A0003` |
| 子商户存在但未激活 | `A0006 Merchant status abnormal` |

调试时这个区别很重要：这条轴上的 `A0003` 表示*您发了
错误的标识符*，而 `A0006` 表示*标识符正确但账户
不可用*——后者无法在代码中修复。如果您运营多个
商户账户，注意 API key 属于其中一个；没有
跨账户的 key。见
[认证与签名](/docs/zh/support/auth-signature-faq)。

## `userId` 是干什么的？

`userId`（String 64，必填）是**您**对付款用户的标识符，
与 `userEmail` 和 `userPhone`（格式 `+CountryCode-Number`，
例如 `+62-08990013157`）一起。它们在您这边标识用户——
不是 Waffo 账户凭证。

## `userTerminal` 应该传什么？

`userTerminal` 告诉 Waffo 用户实际在哪里，以便返回的
支付链接在该环境中可用：

| 值 | 使用场景 |
| - | - |
| `WEB` | 用户在 PC 浏览器上。 |
| `APP` | 用户在您的移动 App 内——您会收到针对 App 环境优化的跳转链接。 |
| `IN_WALLET_APP` | 用户已在某支付方式的钱包 App 内。 |
| `IN_MINI_PROGRAM` | 用户在某支付方式的小程序内。 |

并非每种支付方式都支持每种终端类型；该值还
影响哪些支付方式可以被提供。

## `productName` 是什么？什么时候用 `DIRECT_PAYMENT`？

`productName` 选择支付产品：`ONE_TIME_PAYMENT`、
`MINI_PROGRAM_PAYMENT` 或 `DIRECT_PAYMENT`。大多数商户使用
`ONE_TIME_PAYMENT`。您可以在 Waffo 商户 Portal 的 **Payin Service** 下
查看您账户启用了哪些产品——传入
不在您合同中的产品会校验失败。

## 我为什么收到 `A0003 Parameter validation failed`？

最常见的原因是字段长度和格式限制。经常让商户
中招的有：

* `cancelRedirectUrl`——最大长度 **512**，是成功和
  失败跳转 URL 允许长度的一半
* `goodsInfo.goodsUrl`——最大长度 **128**，必须是完整 URL
  （合规与风控审核预期）
* `userPhone`——必须遵循 `+CountryCode-Number`
* `orderRequestedAt`——必须是带毫秒和 `Z` 后缀的 ISO 8601

错误响应会点名出问题的字段；请对照
[上文字段表](#is-there-one-table-showing-which-fields-are-mandatory-and-how-long-they-can-be)检查。

有一个格式失败**不是** `A0003`：不像地址的 `userEmail`
返回 `B0007 Invalid email format`。如果您看到 `B0007`，
要修的是邮箱值，不是长度或时间戳。

## `userEmail` 必填但我从没收集过，该发什么？

发送一个**唯一**的合成地址，采用已发布的回退形式
`userId@examples.com`，把用户的实际 id 代入。格式会被校验——
值必须看起来像地址，否则创建以
`B0007 Invalid email format` 失败——但 API 不强制唯一性，
这正是为什么保持唯一是您的责任。

两件不要做的事，因为两者都会伤害您而不是明确报错：

* **不要发送字面占位符**，例如 `test@…`。
* \*\*不要让多个用户共用一个地址。\*\*一个地址出现在许多支付上
  正是风控筛查设计来识别的模式，所以代价体现为
  好交易被拒绝，而不是创建调用报错。

一旦您收集到真实地址，就发送真实地址。如果您的历史
流量需要追溯应用回退，那是需要您授权的支持请求，
不是参数变更。

## 什么时候需要发送 `userCurrency`？

`userCurrency` 是可选的，不存在"userCurrency is required"错误。它
声明用户将以何种币种支付，用于该币种不是您
`orderCurrency` 的情形。

它实际影响的是**您的哪些合同可以服务该订单**。
跨币种订单只能通过为该货币对配置的合同路由；
如果您省略该字段且也没有同币种合同匹配，
就没有可路由的目标，您会收到
`A0018 Payment method in merchant order does not match with current merchant contract`——
它读起来像支付方式问题，实际上是币种配对问题。
这是该接口上最常被误读的失败。

跨币种订单的已发布规则是：未固定 `payMethodType` 或 `payMethodName` 时
发送 `payMethodCountry`；`userCurrency` 本身
保持可选。您的合同覆盖哪些币种，以及订单上三种
币种的关系，见
[我该检查哪些金额和币种字段？](/docs/zh/support/order-amount-and-currency-fields-faq)。

## 我为什么收到 `A0018 Payment method does not match`？

`A0018` 表示订单中的支付方式与您当前的
商户合同不匹配。可用的支付方式取决于您账户
启用了什么——请查看商户 Portal，或联系集成
支持调整您的合同。

## 支付失败了——用户怎么换一种支付方式重试？

创建一个**带新 `paymentRequestId` 的新订单**。复用之前的
`paymentRequestId` 并改变 `payMethod` 是幂等不匹配，
以 `A0011` 失败。

从卡切换到其他方式时还有一条额外规则：已保存的卡 token
（`userPaymentAccessToken`）仅对卡支付有效。如果失败的
尝试是卡支付，而重试指定了非卡方式（例如
银行转账或钱包），**不要把卡 token 带过去**
——该组合无法路由，订单创建会以
"no available channel" 错误失败。

## 我应该传哪个 WeChat Pay 方式和订单币种？

WeChat Pay 不是单一配置。`WECHATPAY` 有三种区域
配置，各绑定一种订单币种，而 `WECHATPAYHK` 是
独立的方式：

| 方式 | 订单币种 | 可支付的钱包 |
| - | - | - |
| `WECHATPAY`——HKG | `HKD` | 在香港**和**中国大陆注册的钱包 |
| `WECHATPAY`——CHN | `CNY` | 仅中国大陆钱包 |
| `WECHATPAY`——USA | `USD` | 仅中国大陆钱包 |
| `WECHATPAYHK` | `HKD` | 仅香港钱包 |

请选择与您用户钱包注册地匹配的组合——如果
钱包不在您扣款所用配置的覆盖范围内，用户无法
完成支付。用 `/api/v1/paymethodconfig/inquiry` 确认您账户
启用了其中哪些；每个条目携带该方式的 `country`
和 `currency`。

## 错误码族

| 前缀 | 族 |
| - | - |
| `Axxxxx` | 商户相关（凭证、参数、合同、限额） |
| `Bxxxxx` | 用户相关 |
| `Cxxxxx` | 系统相关 |
| `Dxxxxx` | 风控相关拒绝 |
| `Exxxxx` | 未知——重试交易（或订单查询）直到返回最终的 Success / Failed 状态 |


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