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

三个 ID,各在哪里用

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

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

有——就是这张。这里的”必填”指对每一笔订单都必填。第二组 更大的字段在契约中是可选的,但在特定支付方式或场景下 变为必填;这些单独列在 各支付方式的条件必填字段, 查找表在 API 参考表。 顶层 merchantInfo(对象本身必填) userInfo(对象本身必填) paymentInfo(对象本身必填) goodsInfo——对象可选。契约将 goodsName 和 goodsUrl / appName 之一记为预期内容(用于合规与风控 审核),但创建请求只强制执行下面的长度限制;某支付方式 是否坚持要求它们,是按方式的条件规则 cardInfo、riskData、addressInfo、brandInfo——均为可选对象
注意这个容易让人中招的不对称: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 重发但 支付方式、用户或描述不同,不会 触发不匹配——它返回第一个订单。如果您想要一个真正 不同的订单,请使用新的 paymentRequestId。
  • **A0040 是”再试一次”,不是失败。**它表示您的两次调用撞车了。 不要因此创建第二个订单 id;用相同 id 重新调用,或对它 运行订单查询。

我收到了 A0040 Order cannot be concurrently created,应该重试吗?

是——用相同的 paymentRequestId。它在同一 id 的两次创建 同时在途时抛出,所以其中恰好一次在做实际工作,另一次 被挡回以保护您免于重复订单。短暂停顿后重试, 或直接对该 id 运行订单查询;无论哪种方式,您都会收敛 到那唯一存在的订单上。 您绝不能做的是把它当作”订单未创建”并用 新 id 重新发起。真正的重复扣款就是这样产生的。

我能把 orderAmount 传 0 吗?

零金额不会被通用金额检查拒绝——请求级规则 是金额不能为负——但零金额订单能否 实际创建取决于您使用的支付产品和支付方式, 而且零金额对其中大多数没有意义。 所以不要在此基础上构建流程。如果您的用例是”验卡不扣款”, 为此设计的机制是 绑卡,不是零金额订单。如果您的 用例是免费订单,就完全不要创建 Waffo 订单。 负金额以 A0003 失败。

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

小数位数按该币种的 ISO 4217 精度检查, 小数过多的值以 A0003 失败——完整规则,包括 必须不带小数的币种以及跨币种订单的不同处理,见 金额与币种和 订单金额、限额与拒绝。 还有第二项不太明显的检查。某些支付方式与币种组合 只接受某个步长的整数倍金额——例如只接受十的倍数, 或只接受完全不带小数的整数单位—— 且该规则绑定在组合上,而不是仅在币种上。这就是为什么 在某币种下完全有效的金额仍可能在某一特定方式上被拒绝。 如果您的价格不是整数,请用真实金额而不是凑整金额测试 您打算提供的每种方式,并把拒绝当作配置事实而不是 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 跟随支付方式:契约中可选, 在支付方式或场景要求收货地址时变为必填—— 见条件必填字段。

我必须发送 goodsInfo 吗?goodsUrl 或 appName 够吗?

对象本身是可选的,创建请求不强制执行其内部字段, 仅限于长度限制——只带 goodsName 的 goodsInfo 会被接受。契约将 goodsName 加 goodsUrl 或 appName 之一记为预期内容,因为那是合规与风控 审核所查看的:网页商户发送产品 URL;纯 App 商户发送 包名,例如 com.example.app123。它们是二选一, 不是两者都要。 当 goodsInfo 内的某个字段确实因缺失被拒绝时,规则来自 支付方式,而不是该对象:某些方式要求商品 详情,并像其他任何条件必填字段一样报告缺失路径——见 条件必填字段。 所以实用建议是:要么省略该对象,要么完整发送。省略 它本身不会让订单被拦截,但它从风控筛查中移除了商品 上下文,而对某些业务类别来说,那正是 让边缘交易获批的上下文。

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

可以,通过 paymentInfo.payMethodProperties,它是一个 JSON 字符串。涉及 两个键: 仅限数字,且每个前缀必须在 4 到 9 位范围内;其他任何情况 都是 A0003。请有意地设置 cardPrefixPurpose:被告知”此 卡不符合本次促销条件”的购物者与被告知 “此卡不被接受”的购物者反应截然不同,而这两种措辞来自这一个值。 同一个 properties 字段也是少数其他方式专属键的所在, 包括某些市场要求的付款人税号——见 条件必填字段。

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

您可以在类卡支付上通过 cardInfo.threeDsDecision 表达一个决定。它恰好接受两个值: 其他任何值都会被作为无效卡数据拒绝。在依赖它之前需要理解 两个限制:
  • 它仅适用于客户在场的类卡支付。 在商户发起的交易上——您对存储 token 发起、无客户 在会话中的扣款——该字段不生效。
  • 它是您表达的决定,不是对发卡行的保证。经过认证的 支付随后是否成功,以及未认证的支付是否被银行 拒绝,由发卡行决定。
如果您想在自己的页面上收集卡号,这在成为参数问题之前 首先是合规问题:您需要相应的 卡数据资质,而替代方案——让 Waffo 的收银台或 绑卡流程处理卡——正是 绑卡和 收银台集成存在的目的。

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

发送 paymentInfo.merchantInitiatedMode,它接受两个值: 只要发送 merchantInitiatedMode,userPaymentAccessToken 就是必填的; 没有它请求会校验失败,因为 没有可扣款的存储凭证。发送该模式还会把交易标记为 商户发起,这正是改变其认证方式的原因——也是 threeDsDecision 不再生效的原因。 关于 token 最初如何创建,以及客户在场的首次支付与 后续扣款的区别,见 绑卡。

payMethodName 能带多个值吗?

可以。payMethodName 和 payMethodType 都接受逗号分隔的列表, 且值匹配不区分大小写。这就是把收银台收窄到 一个子集而非恰好一种方式的做法——例如同时传入某卡组织的信用卡 和借记卡条目,让购物者可以用任一种。 常见情形的已发布参数指引值得严格遵循: 对开放的卡受理,传 payMethodType="CREDITCARD,DEBITCARD" 并将 payMethodName 留空,让卡组织根据购物者输入的 BIN 确定;要固定单一方式,两个字段都传;要让购物者 自由选择,两个都不传。您账户的确切值来自 /api/v1/paymethodconfig/inquiry 或 Portal 的 Payin 页面——不要硬编码 从规范复制的列表。

merchantInfo.merchantId 和我的 API key 不匹配会怎样?

立即以 A0003 失败,在任何订单处理之前:请求体中的商户 ID 必须是您签名所用 API key 所属的商户。 同一检查有三个相邻的失败情形值得区分: 调试时这个区别很重要:这条轴上的 A0003 表示您发了 错误的标识符,而 A0006 表示标识符正确但账户 不可用——后者无法在代码中修复。如果您运营多个 商户账户,注意 API key 属于其中一个;没有 跨账户的 key。见 认证与签名。

userId 是干什么的?

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

userTerminal 应该传什么?

userTerminal 告诉 Waffo 用户实际在哪里,以便返回的 支付链接在该环境中可用: 并非每种支付方式都支持每种终端类型;该值还 影响哪些支付方式可以被提供。

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
错误响应会点名出问题的字段;请对照 上文字段表检查。 有一个格式失败不是 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 本身 保持可选。您的合同覆盖哪些币种,以及订单上三种 币种的关系,见 我该检查哪些金额和币种字段?。

我为什么收到 A0018 Payment method does not match?

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

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

创建一个带新 paymentRequestId 的新订单。复用之前的 paymentRequestId 并改变 payMethod 是幂等不匹配, 以 A0011 失败。 从卡切换到其他方式时还有一条额外规则:已保存的卡 token (userPaymentAccessToken)仅对卡支付有效。如果失败的 尝试是卡支付,而重试指定了非卡方式(例如 银行转账或钱包),不要把卡 token 带过去 ——该组合无法路由,订单创建会以 “no available channel” 错误失败。

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

WeChat Pay 不是单一配置。WECHATPAY 有三种区域 配置,各绑定一种订单币种,而 WECHATPAYHK 是 独立的方式: 请选择与您用户钱包注册地匹配的组合——如果 钱包不在您扣款所用配置的覆盖范围内,用户无法 完成支付。用 /api/v1/paymethodconfig/inquiry 确认您账户 启用了其中哪些;每个条目携带该方式的 country 和 currency。

错误码族