--- url: /zh-cn/index.md --- # 概览 本文档仅供已签约商户使用,请勿公开转载。 ## 交易类型 ST Pay 提供两种交易,均由您的服务器调用 API 创建: | 交易 | 资金流向 | 流程 | | --- | --- | --- | | 代收 | 付款人 → ST Pay 收款账户 → 您的 ST Pay 余额 | 您创建代收单,获取收款账号与**订单金额**,并请付款人转账。ST Pay 确认入账后,将扣除手续费后的结算金额计入您的余额,并通过 Callback 通知您。 | | 代付 | 您的 ST Pay 余额 → 收款人 | 您创建代付单并指定收款账号。ST Pay 出款后,从您的余额中扣除代付金额与手续费,并通过 Callback 通知您。 | 支持两种通道:`fps`(转数快)与 `bank_transfer`(银行转账)。 ## API 列表 | API | 用途 | 方法与路径 | | --- | --- | --- | | [代收建单](./api/collect.md) | 创建代收单,获取付款信息 | `POST /api/v1/collect/orders` | | [代付建单](./api/payout.md) | 创建代付单 | `POST /api/v1/payout/orders` | | [代收查单](./api/query.md#代收查单) | 查询代收单当前的状态与金额 | `POST /api/v1/collect/orders/query` | | [代付查单](./api/query.md#代付查单) | 查询代付单当前的状态与金额 | `POST /api/v1/payout/orders/query` | | [Callback](./api/callback.md) | 订单有结果时,ST Pay 主动通知您 | ST Pay `POST` 到您建单时传入的 `callback_url` | ## 对接流程 1. **向 ST Pay 提供以下资料** * 服务器出口 IP:调用 API 的所有来源 IP,每个都必须是单个 IPv4 或 IPv6 地址,不支持网段。 * 后台登录 IP:登录[商户后台](./guide/backoffice.md)的来源 IP,格式同上。与 API 的 IP 白名单分开设置。 2. **获取 API 凭证与后台账号**:ST Pay 会提供 `api_identity`(API 身份)与 **API 验证密钥**。密钥仅在创建时显示一次,之后 ST Pay 不再显示,请立即妥善保存。ST Pay 也会创建商户主管的后台账号并提供初始密码,首次登录时必须修改。 3. **在测试环境完成对接**:按照本文档完成四个 API 与 Callback 的对接,并对照[上线前检查清单](./reference/checklist.md)逐项确认。 4. **上线生产环境**:生产环境的域名、凭证、IP 白名单与测试环境完全独立,需另行申请。 API 与后台登录的 IP 白名单、凭证均由 ST Pay 配置。如需变更,请联系您的 ST Pay 对接联系人;IP 白名单变更后约 1 分钟生效。 Callback 地址无需提供给 ST Pay:请在每次建单时通过 `callback_url` 传入,规则见 [Callback 地址](./api/callback.md#callback-地址)。 ## 环境 | 环境 | API 域名 | 商户后台 | 说明 | | --- | --- | --- | --- | | 测试环境 | 由 ST Pay 对接窗口提供 | 由 ST Pay 对接窗口提供 | 不产生真实资金流转;使用测试环境专用的凭证 | | 生产环境 | 测试环境完成对接后另行提供 | 测试环境完成对接后另行提供 | 真实资金流转;使用生产环境专用的凭证 | 完整地址为“域名 + 路径”,例如 `https://merchant.vincoterra.com/api/v1/collect/orders`。商户后台与 API 使用同一个域名(只是路径不同),付款页则在 `pay.vincoterra.com`。本文档示例一律使用生产环境的实际域名:`merchant.vincoterra.com`(API 与商户后台)、`pay.vincoterra.com`(付款页);测试环境的域名由 ST Pay 对接窗口另行提供。 --- --- url: /zh-cn/api/callback.md --- # Callback 通知 订单有结果时,ST Pay 会向该订单建单时传入的 `callback_url` 发送 `POST` 请求。 ## 发送时机 | 事件 | `status` | 是否发送 | | --- | --- | --- | | 代收成功 | `success` | 发送 | | 代收被拒绝(ST Pay 人工拒绝,或 30 分钟到期被系统拒绝) | `rejected` | 发送 | | 代收超时入账 | `late_credited` | **不发送**,请通过查单确认,见[订单状态](../reference/status.md#代收单) | | 代付已出款 | `paid` | 发送 | | 代付被拒绝 | `rejected` | 发送 | ## Callback 地址 * 每次建单([代收](./collect.md)、[代付](./payout.md))时通过 `callback_url` 传入,无需事先提供给 ST Pay。每笔订单可以不同,例如测试与生产环境各用自己的地址。 * **建单后不能更改**。自动发送与 ST Pay 手动重发都发往该订单建单时的地址。若您之后更换地址,旧订单的通知仍发往旧地址;旧地址收不到时,请通过[查单](./query.md)确认旧订单的结果。 * 最多 500 个字符,按原样保存,不做任何转换。 * `http` 与 `https` 都可以,建议用 `https`:`http` 会以明文传送订单号与金额。不论哪一种,都**务必验证 `sign`**,才能防止伪造或篡改的通知。 * 使用 `https` 时,网站的 HTTPS 证书(SSL/TLS 证书)必须由公开的证书颁发机构签发、在有效期内、名称与网址中的主机名相符,并且服务器要发送完整的证书链(含中间证书)。自签名证书、过期、名称不符或缺少中间证书都视为送达失败,**不会重试**。 * 不能包含账号密码(`user:pass@`)、`#`、空白或非 ASCII 字符;中文域名请转换为 punycode(以 `xn--` 开头)。可以带 path 与 query。 * 主机名只能包含字母、数字、`.`、`-`(不能有下划线 `_`),或以 `[ ]` 包住的 IPv6;写了端口号时须为 1 至 65535(不能只写 `:`)。 * path 与 query 中不能直接出现 `|`、`{`、`}`、`^`、`"`、`<`、`>`、`\`、反引号,`%` 后面必须是两位十六进制数字;这些字符请先做 URL 编码(例如 `%` 写成 `%25`)。 * 未传、空字符串或全是空白返回 `missing_parameter`。 * 不能是本机或内网地址,测试与生产环境都一样: * `localhost`,以及以 `.localhost`、`.local`、`.localdomain`、`.internal`、`.intranet`、`.lan`、`.corp`、`.home.arpa`、`.private` 结尾的域名,或不含「.」的主机名。 * 私有、回环、链路本地、CGNAT、保留与文档用途、组播的 IP,例如 `10.x.x.x`、`172.16.x.x`–`172.31.x.x`、`192.168.x.x`、`127.x.x.x`、`169.254.x.x`、`100.64.x.x`;IPv6 只接受全球单播地址(`2000::/3`),其中 Teredo `2001::/32`、文档用途 `2001:db8::/32`、6to4 `2002::/16` 也不接受。 * 纯数字、十六进制、八进制或省略段数的 IP 写法,例如 `2130706433`、`0x7f.0.0.1`、`0177.0.0.1`、`127.1`。 * 公网 IP 可以直接填写。 * 不符合以上规则时,建单返回 `invalid_callback_url`(格式错误、超过 500 个字符则返回 `invalid_parameter`),不会建单。 * ST Pay **每次发送前**(包括手动重发)都会重新解析域名;解析失败,或解析出的任何一个地址不是公网地址,本次就不发送,视为送达失败。请确认域名可从公网解析,并且只指向公网 IP。 ## 请求格式 * 方法为 `POST`,`Content-Type: application/json`,请求体为 JSON 对象,所有值都是字符串。 * 包含 `sign`,[签名](../guide/signature.md)规则与 API 请求相同,使用您的 API 验证密钥(见[更换密钥](../guide/security.md#更换密钥))。Callback 不包含 `api_identity` 与 `timestamp`。 * ST Pay 建立连接最多等待 5 秒、等待响应最多 10 秒,且**不跟随重定向(3xx)**:收到 3xx 视为送达失败。请直接填写最终地址,例如 `http` 跳转 `https`、缺少末尾的 `/` 都可能导致重定向。 * Callback 内容不包含 `callback_url`。 * 请求头固定带 `User-Agent: QPay-Callback/1.0` 与 `Accept: */*`。Callback 地址在 Cloudflare、WAF 或其他防火墙后面时,请放行这个 User-Agent 与 `POST` 请求,否则可能被拦截为 403 而送达失败。 ## 您需要返回的响应 返回 HTTP **2xx**,且响应体为纯文本 `ok`(首尾空白字符会被忽略)。其他任何响应,包括超时、非 2xx、响应体不是 `ok`,都视为**送达失败**。 ```http HTTP/1.1 200 OK Content-Type: text/plain ok ``` ::: warning Callback 不会自动重试 每笔订单只会自动发送一次。送达失败时,只有 ST Pay 工作人员能手动重发,重发时一律发往该订单建单时传入的 `callback_url`。因此: * 请不要只依赖 Callback。对于长时间没有结果的订单,请定期查单确认。 * 同一笔订单可能收到多次 Callback(手动重发),请以 `order_no` 加 `status` 做幂等处理,已处理过的通知直接返回 `ok`。 * Callback 送达失败不影响交易结果,订单状态与余额不会因此回滚。 ::: ## 字段 | 字段 | 代收成功 | 代收拒绝 | 代付已出款 | 代付拒绝 | 说明 | | --- | --- | --- | --- | --- | --- | | `order_no` | ✓ | ✓ | ✓ | ✓ | ST Pay 订单号 | | `merchant_order_no` | ✓ | ✓ | ✓ | ✓ | 商户单号 | | `status` | `success` | `rejected` | `paid` | `rejected` | 订单状态 | | `received_amount` | ✓ | | | | 实收金额 | | `fee` | ✓ | | ✓ | | 手续费 | | `settlement_amount` | ✓ | | | | 结算金额 | | `payable_amount` | | ✓ | | | 订单金额(含尾数) | | `amount` | | | ✓ | ✓ | 代付金额 | | `finished_at` | ✓ | ✓ | ✓ | ✓ | 完成时间 | | `sign` | ✓ | ✓ | ✓ | ✓ | 签名 | Callback 不包含拒绝原因,如有需要请通过查单获取。 代收与代付若传入同一个 `callback_url`,两者的 `status` 都可能是 `rejected`,而且代收单与代付单的商户单号可以相同。请以建单时获取的 `order_no` 匹配订单;如果要用商户单号匹配,请让代收与代付使用不会重复的单号,例如添加不同的前缀。 ## 处理建议 1. 解析 JSON,用**收到的全部字段**(`sign` 除外)重新计算签名并比对。不要只挑选本文档列出的字段,日后可能会新增字段。 2. 签名不一致则丢弃,不要更新订单。 3. 以 `order_no` 找到您的订单;已处理过相同结果时直接返回 `ok`。 4. 更新订单后返回 `ok`。耗时的后续处理(发货、通知会员等)请放到后台异步执行,避免超过 10 秒。 5. 代收成功请以 `received_amount`(实收金额,或扣除手续费后的 `settlement_amount`)入账,不要用建单时的金额。实收金额与您预期的金额不同时,请转人工确认,不要自动入账。 6. 订单已是终态(`success`、`paid`、`rejected`)后,又收到较晚到达、状态不同的通知时,不要回退订单状态;请记录下来转人工,有疑义以[查单](./query.md)结果为准。 ## 示例 以下签名均是用[示例密钥](../guide/signature.md#示例)计算的实际值。 ::: code-tabs @tab 代收成功 ```json { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "status": "success", "received_amount": "1000.37", "fee": "25.00", "settlement_amount": "975.37", "finished_at": "2026-10-13 10:12:30", "sign": "49d9d35c3b3a97a55fe60976a331d36913b0d3cd9b82e840e0315c445653d207" } ``` @tab 代收拒绝 ```json { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "status": "rejected", "payable_amount": "2000.58", "finished_at": "2026-10-13 10:31:17", "sign": "cd85bf732776bb2897f6e767dda873ce580330f979be3f812c390ec09af0d2f4" } ``` @tab 代付已出款 ```json { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "status": "paid", "amount": "500.00", "fee": "7.50", "finished_at": "2026-10-13 10:20:41", "sign": "64e3b813fb086b98c1a4e68c2689423ff347336219f182293e5344a3f6853562" } ``` @tab 代付拒绝 ```json { "order_no": "P202610131006403520000", "merchant_order_no": "PO20261013-0002", "status": "rejected", "amount": "3000.00", "finished_at": "2026-10-13 10:25:09", "sign": "bdf0a8031ac0beff801a4b2591793ea6d6edbfd27dbdadce24ab871602f99094" } ``` ::: 代收成功示例的待签名字符串: ```text fee=25.00&finished_at=2026-10-13 10:12:30&merchant_order_no=MO20261013-0001&order_no=C202610131000051230000&received_amount=1000.37&settlement_amount=975.37&status=success ``` --- --- url: /zh-cn/api/collect.md --- # 代收建单 ```text POST /api/v1/collect/orders ``` 创建一笔代收单,获取需提供给付款人的付款信息。 ::: tip 付款人必须转入“订单金额”payable_amount,而不是您提交的原始金额 ST Pay 会在原始金额上加上 0.01 至 0.99 的识别尾数,例如原始金额为 1000.00,订单金额可能是 1000.37。ST Pay 依据这个精确金额识别入账属于哪一笔订单。如果您使用 json 模式自行展示付款页面,请清晰显示订单金额,并提醒付款人按此金额转账。 ::: * 订单在建单后 **30 分钟**失效(`expires_at`)。到期仍未入账的订单会被系统拒绝,并发送失败 Callback。 * `response_mode` 决定返回内容: * `url`:返回 ST Pay 付款页地址 `payment_url`,将付款人跳转到该页面即可。 * `json`:返回收款账号信息,由您自行展示付款页面。 * 同一会员最多同时有 **10 笔**待审批(`pending`)的代收单,超过时返回 `user_active_orders_limit`。 ## 请求字段 | 字段 | 必填 | 格式 | 说明 | | --- | --- | --- | --- | | `api_identity` | 是 | 字符串 | ST Pay 分配的 API 身份 | | `merchant_order_no` | 是 | 1–200 个字符,字母、数字、`_`、`-` | 您的订单号。在您的代收单中不能重复,无论原订单处于什么状态 | | `merchant_user_id` | 二选一 | 1–200 个字符,字母、数字、`_`、`-` | 付款会员在您系统中的唯一标识。传入时,以它判断是否为同一会员 | | `payer_name` | 二选一 | 1–200 个字符,只能包含中文、英文字母、空格 | 付款人真实姓名。未传 `merchant_user_id` 时,以姓名判断是否为同一会员(不区分大小写,忽略所有空格)。不能包含数字或标点,例如 `Chan Tai-man` 中的连字符会被拒绝 | | `amount` | 是 | 金额字符串 | 原始金额。单笔上下限以此金额判断(不含尾数) | | `channel_type` | 是 | `fps` 或 `bank_transfer` | 通道类型 | | `response_mode` | 是 | `url` 或 `json` | 返回模式,说明见上方 | | `language` | 否 | `zh-TW`、`zh-CN` 或 `en` | 付款页的语言:繁体中文、简体中文、英文。不传或空字符串为 `zh-TW`。只影响 `url` 模式的付款页 | | `callback_url` | 是 | 网址,最多 500 个字符 | 该订单的 [Callback](./callback.md) 发往此地址,建单后不能更改。`http` 或 `https` 都可以,不能是本机或内网地址,见 [Callback 地址](./callback.md#callback-地址) | | `timestamp` | 是 | `yyyy-MM-dd HH:mm:ss` | 发送请求当下的时间(UTC+8),与 ST Pay 服务器时间相差超过 5 分钟会被拒绝,见[请求与响应格式](../guide/conventions.md#请求) | | `sign` | 是 | 64 位小写十六进制 | 见[签名](../guide/signature.md) | `merchant_user_id` 与 `payer_name` 至少传一个;空字符串或全是空格均视为未传。两个都未传时返回 `missing_member_identity`。两个都传时,以 `merchant_user_id` 判断会员,姓名仅用于展示。 ## 请求示例 ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "merchant_user_id": "U10086", "payer_name": "王小明", "amount": "1000.00", "channel_type": "fps", "response_mode": "url", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:00:00", "sign": "566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2" } ``` ## 响应字段 | 字段 | 返回条件 | 说明 | | --- | --- | --- | | `order_no` | 始终返回 | ST Pay 订单号 | | `merchant_order_no` | 始终返回 | 您提交的商户单号 | | `status` | 始终返回 | 固定为 `pending` | | `payment_url` | `url` 模式 | 付款页地址,见[付款页](#付款页) | | `channel_type` | `json` 模式 | `fps` 或 `bank_transfer` | | `bank_name` | `json` 模式且为 `bank_transfer` | 收款银行名称 | | `bank_code` | `json` 模式且为 `bank_transfer` | 收款银行代码(字符串,保留开头的 0) | | `account_no` | `json` 模式 | 完整收款账号 | | `account_name` | `json` 模式 | 收款户名 | | `payable_amount` | 始终返回 | 订单金额,等于原始金额加识别尾数。付款人须转入此金额 | | `expires_at` | 始终返回 | 失效时间,等于建单时间加 30 分钟 | | `sign` | 始终返回 | 响应签名,以本次请求使用的密钥计算,见[验证响应](../guide/signature.md#验证响应)。验签通过才使用 `account_no`、`payment_url` | ### 响应示例:url 模式 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "status": "pending", "payment_url": "https://pay.vincoterra.com/pay/QzIwMjYxMDEzMTAwMDA1MTIzMDAwMA.v3Kd9QeLr7Zp1sWm4XyT8bN2cF6hJ0aG5uE-iOkRtYw", "payable_amount": "1000.37", "expires_at": "2026-10-13 10:30:05", "sign": "7e876d9780d033c6224f52015eb15e1d1a9bbda9b75764fff0cdbf3d36b183ee" } } ``` ### 响应示例:json 模式、银行转账 请求为 `"merchant_order_no": "MO20261013-0002"`、`"payer_name": "Chan Tai Man"`、`"amount": "2000.00"`、`"channel_type": "bank_transfer"`、`"response_mode": "json"`、`"callback_url": "https://shop.example.com/qpay/callback"`、`"timestamp": "2026-10-13 10:01:17"`,签名为 `e20f45942090246543ab7bd95a1fecd52011699382a93637529c2aa987767848`。 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "status": "pending", "channel_type": "bank_transfer", "bank_name": "The Hongkong and Shanghai Banking Corporation Limited", "bank_code": "004", "account_no": "123-456789-001", "account_name": "WONG KA YAN", "payable_amount": "2000.58", "expires_at": "2026-10-13 10:31:17", "sign": "4cd29b8f52d256516363af07eccfb821caaef52a10777cdc5495b809447aa175" } } ``` json 模式且通道为 `fps` 时,不返回 `bank_name` 与 `bank_code`,`account_no` 为 FPS 收款账号。 ## 付款页 使用 `url` 模式时: * 页面显示订单金额、通道类型、收款账号与户名、失效时间;银行转账还会显示银行名称与代码。 * 订单金额、银行代码、收款账号与户名旁有“复制”键。 * 订单一旦结束(成功、拒绝、超时),页面只会显示“订单已结束”,不再显示收款信息。付款人停留在页面上时会自动切换到这个画面,无需刷新。 * 页面语言按建单时的 `language` 显示,付款人无法切换。 * 该地址本身就是访问凭证,持有该地址即可看到完整收款账号。请只提供给该笔订单的付款人,不要写入公开的日志、分析工具或分享链接。 * 请完整使用返回的地址,不要修改或自行拼接,任何改动都会导致页面显示“找不到頁面”(找不到订单时不知道建单语言,一律以繁体中文显示)。 ## 可能的错误 `invalid_signature`、`timestamp_expired`、`ip_not_allowed`、`merchant_disabled`、`missing_parameter`、`invalid_parameter`、`invalid_amount`、`missing_member_identity`、`invalid_callback_url`、`amount_out_of_range`、`merchant_order_no_duplicate`、`user_active_orders_limit`、`channel_full`、`no_available_card`、`internal_error`。说明见[错误码](../reference/errors.md)。 --- --- url: /zh-cn/api/payout.md --- # 代付建单 ```text POST /api/v1/payout/orders ``` 创建一笔代付单,由 ST Pay 从您的余额向指定收款人付款。 * 建单时会检查**可用余额 ≥ 代付金额 + 手续费**,不足时返回 `insufficient_balance`,不会建单。 * 建单**不会冻结或预留**余额。ST Pay 实际出款时才扣款,并会再次检查余额。若您同时创建多笔代付单,合计金额可能超过余额;出款时余额不足的订单会保持待审批状态,并可能被拒绝。 * 手续费率在建单时锁定,之后的费率调整不影响该订单。建单响应不包含手续费,出款后可通过 Callback 或查单获取。 ## 请求字段 | 字段 | 必填 | 格式 | 说明 | | --- | --- | --- | --- | | `api_identity` | 是 | 字符串 | ST Pay 分配的 API 身份 | | `merchant_order_no` | 是 | 1–200 个字符,字母、数字、`_`、`-` | 您的订单号。在您的代付单中不能重复,代收与代付分别独立计算 | | `amount` | 是 | 金额字符串 | 代付金额,也是手续费的计算基数 | | `channel_type` | 是 | `fps` 或 `bank_transfer` | 通道类型 | | `account_no` | 是 | 因通道而异,见[收款账号格式](#收款账号格式) | 收款账号 | | `account_name` | 是 | 1–200 个字符 | 收款人姓名或户名 | | `bank_name` | 银行转账必填 | 1–100 个字符 | 收款银行名称。FPS 不可传 | | `bank_code` | 银行转账必填 | 字符串 | 收款银行代码,须为[银行代码](../reference/banks.md)列表中的值,保留开头的 0。FPS 不可传 | | `callback_url` | 是 | 网址,最多 500 个字符 | 该订单的 [Callback](./callback.md) 发往此地址,建单后不能更改。`http` 或 `https` 都可以,不能是本机或内网地址,见 [Callback 地址](./callback.md#callback-地址) | | `timestamp` | 是 | `yyyy-MM-dd HH:mm:ss` | 发送请求当下的时间(UTC+8),与 ST Pay 服务器时间相差超过 5 分钟会被拒绝,见[请求与响应格式](../guide/conventions.md#请求) | | `sign` | 是 | 64 位小写十六进制 | 见[签名](../guide/signature.md) | ### 收款账号格式 | 通道 | `account_no` 支持的格式 | | --- | --- | | `fps` | 以下三种任选其一,填在同一个字段中:・手机号码:8 位数字,例如 `91234567`・Email:最长 200 个字符,只能包含字母、数字与 `@ . _ -`;恰好一个 `@` 且前后都有字符,`@` 后面要有 `.`,例如 `chan.taiman@example.com`・FPS 识别码:1 至 50 位数字 | | `bank_transfer` | 数字与连字符 `-`,1 至 50 个字符,例如 `012-345678-001` | FPS 订单传了 `bank_name` 或 `bank_code` 会返回 `invalid_parameter`;银行转账订单缺少其中任何一个会返回 `missing_parameter`。 ## 请求示例 ::: code-tabs @tab FPS ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "PO20261013-0001", "amount": "500.00", "channel_type": "fps", "account_no": "91234567", "account_name": "陳大文", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:05:22", "sign": "693c80aaf437abcd6dbf60f8aed9e4eda09deed052977fa63cb9b1768150044f" } ``` @tab 银行转账 ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "PO20261013-0002", "amount": "3000.00", "channel_type": "bank_transfer", "account_no": "012-345678-001", "account_name": "CHAN TAI MAN", "bank_name": "Bank of China (Hong Kong) Limited", "bank_code": "012", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:06:40", "sign": "44aadf0a2be821c13c1aef71d8cc7cfc6cf012d3b15f543d2650cdf762002a2b" } ``` ::: ## 响应字段 | 字段 | 说明 | | --- | --- | | `order_no` | ST Pay 订单号 | | `merchant_order_no` | 您提交的商户单号 | | `status` | 固定为 `pending` | | `amount` | 代付金额 | | `sign` | 响应签名,以本次请求使用的密钥计算,见[验证响应](../guide/signature.md#验证响应) | ```json { "code": "OK", "message": "成功", "data": { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "status": "pending", "amount": "500.00", "sign": "e2fe8e5d430ba4bf8a1003b0d7fc8eabcc5d7d974ceda5135783d1aecfa44c8a" } } ``` ## 可能的错误 `invalid_signature`、`timestamp_expired`、`ip_not_allowed`、`merchant_disabled`、`missing_parameter`、`invalid_parameter`、`invalid_amount`、`invalid_callback_url`、`amount_out_of_range`、`merchant_order_no_duplicate`、`insufficient_balance`、`internal_error`。说明见[错误码](../reference/errors.md)。 --- --- url: /zh-cn/api/query.md --- # 查单 查询订单当前的状态与金额。以下情况请通过查单确认结果: * 未收到 Callback。 * 代收单发生[超时入账](../reference/status.md#代收单)(不发送 Callback)。 * 建单请求超时,或收到 `internal_error`。待付款的订单会一并返回付款信息,可以继续让付款人支付该订单(见下方)。 * 建单返回 `merchant_order_no_duplicate`,需要了解原订单状态。 查单是只读操作,可以重复调用。 ## 请求字段 代收查单与代付查单的请求字段相同: | 字段 | 必填 | 说明 | | --- | --- | --- | | `api_identity` | 是 | ST Pay 分配的 API 身份 | | `order_no` | 二选一 | ST Pay 订单号 | | `merchant_order_no` | 二选一 | 您的商户单号 | | `timestamp` | 是 | 发送请求当下的时间(UTC+8,`yyyy-MM-dd HH:mm:ss`),与 ST Pay 服务器时间相差超过 5 分钟会被拒绝,见[请求与响应格式](../guide/conventions.md#请求) | | `sign` | 是 | 见[签名](../guide/signature.md) | * 两个都未传:返回 `missing_parameter`。 * 两个都传:必须指向同一笔订单,指向不同订单时返回 `query_conflict`;其中一个查不到时返回 `order_not_found`。 * 只能查到您自己的订单。订单不存在、属于其他商户,或已超过 6 个月被删除,均返回 `order_not_found`。 可能的错误:`invalid_signature`、`timestamp_expired`、`ip_not_allowed`、`missing_parameter`、`invalid_parameter`、`query_conflict`、`order_not_found`、`internal_error`。 ## 代收查单 ```text POST /api/v1/collect/orders/query ``` ### 请求示例 ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "timestamp": "2026-10-13 10:35:00", "sign": "c29089b08a6fdcd3d9c17a404cae90a4aaa0a79210778e53997e4f9f43bb6462" } ``` ### 响应字段 | 字段 | 说明 | | --- | --- | | `order_no` | ST Pay 订单号 | | `merchant_order_no` | 商户单号 | | `channel_type` | `fps` 或 `bank_transfer` | | `status` | `pending`、`success`、`rejected`、`late_credited`,见[订单状态](../reference/status.md) | | `amount` | 原始金额 | | `payable_amount` | 订单金额(原始金额加尾数) | | `received_amount` | 实收金额,仅 `success`、`late_credited` 有值,其他状态为 `null` | | `fee` | 手续费,`success`、`late_credited` 有值;`rejected` 为 `"0.00"`;`pending` 为 `null` | | `settlement_amount` | 结算金额(计入您余额的金额),仅 `success`、`late_credited` 有值,其他状态为 `null` | | `created_at` | 建单时间 | | `finished_at` | 完成时间,`pending` 为 `null` | | `reject_reason` | `rejected` 时的拒绝原因文本:ST Pay 人工拒绝时为所选原因(可能未选择,此时为 `null`);系统超时拒绝时为“訂單已逾時”(系统返回的繁体中文原文)。这是供人阅读的文本,不是代码值。其他状态请忽略此字段 | | `payment_url` | 付款页网址,与建单响应相同。仅**待付款且尚未到失效时间**、建单时使用 `url` 模式的订单才有 | | `bank_name`、`bank_code` | 银行名称与代码。仅待付款且尚未到失效时间、建单时使用 `json` 模式且通道为 `bank_transfer` 的订单才有 | | `account_no`、`account_name` | 完整收款账号与户名(或收款姓名)。仅待付款且尚未到失效时间、建单时使用 `json` 模式的订单才有 | | `expires_at` | 失效时间。仅待付款且尚未到失效时间的订单才有 | | `sign` | 响应签名,以本次请求使用的密钥计算,见[验证响应](../guide/signature.md#验证响应) | 建单请求超时未收到响应时,用商户单号查单:查到 `pending` 且带有上述付款信息,就可以将付款人引导至 `payment_url`(或显示收款账号),继续支付该订单,无需更换商户单号重新建单。失效时间沿用建单时的 `expires_at`,不会延长;超过失效时间或订单已结束,就不再返回付款信息。请先验签,通过后才使用这些信息。 ### 响应示例 ::: code-tabs @tab 待付款(url 模式) ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "channel_type": "fps", "status": "pending", "amount": "1000.00", "payable_amount": "1000.37", "received_amount": null, "fee": null, "settlement_amount": null, "created_at": "2026-10-13 10:00:05", "finished_at": null, "reject_reason": null, "payment_url": "https://pay.vincoterra.com/pay/QzIwMjYxMDEzMTAwMDA1MTIzMDAwMA.v3Kd9QeLr7Zp1sWm4XyT8bN2cF6hJ0aG5uE-iOkRtYw", "expires_at": "2026-10-13 10:30:05", "sign": "8cba06866fc16ea3b585309cd13d851d7bda49bf2e95605275f2d99047620487" } } ``` @tab 成功 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "channel_type": "fps", "status": "success", "amount": "1000.00", "payable_amount": "1000.37", "received_amount": "1000.37", "fee": "25.00", "settlement_amount": "975.37", "created_at": "2026-10-13 10:00:05", "finished_at": "2026-10-13 10:12:30", "reject_reason": null, "sign": "889c767037b6748de1d9fa399129b97012c85f3599a0cce9aff289cf79477626" } } ``` @tab 系统超时拒绝 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "channel_type": "bank_transfer", "status": "rejected", "amount": "2000.00", "payable_amount": "2000.58", "received_amount": null, "fee": "0.00", "settlement_amount": null, "created_at": "2026-10-13 10:01:17", "finished_at": "2026-10-13 10:31:17", "reject_reason": "訂單已逾時", "sign": "6a99a7a5085900260830aee441271fb2a0e94d88d33e49b15bfb5a46d6a6e05e" } } ``` @tab 订单不存在 ```json { "code": "order_not_found", "message": "查無此單", "data": null } ``` ::: ## 代付查单 ```text POST /api/v1/payout/orders/query ``` ### 请求示例 ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "order_no": "P202610131005224410000", "timestamp": "2026-10-13 10:40:00", "sign": "adae0ba479bd688b63425e34dcb77cbf5114b34bee68e266ba045ea69748f2b9" } ``` ### 响应字段 | 字段 | 说明 | | --- | --- | | `order_no` | ST Pay 订单号 | | `merchant_order_no` | 商户单号 | | `channel_type` | `fps` 或 `bank_transfer` | | `status` | `pending`、`paid`、`rejected`,见[订单状态](../reference/status.md) | | `amount` | 代付金额 | | `fee` | 手续费,`paid` 有值;`rejected` 为 `"0.00"`;`pending` 为 `null` | | `total_deducted` | 实扣总额(代付金额加手续费),仅 `paid` 有值,其他状态为 `null` | | `created_at` | 建单时间 | | `finished_at` | 完成时间,`pending` 为 `null` | | `reject_reason` | `rejected` 时的拒绝原因文本(可能未选择,此时为 `null`)。其他状态请忽略此字段 | | `sign` | 响应签名,以本次请求使用的密钥计算,见[验证响应](../guide/signature.md#验证响应) | ### 响应示例 ```json { "code": "OK", "message": "成功", "data": { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "channel_type": "fps", "status": "paid", "amount": "500.00", "fee": "7.50", "total_deducted": "507.50", "created_at": "2026-10-13 10:05:22", "finished_at": "2026-10-13 10:20:41", "reject_reason": null, "sign": "67d2dfa30d5df0d5456f706400e22faaf5acbb22b164885d5e08f1102e8538a7" } } ``` --- --- url: /zh-cn/guide/backoffice.md --- # 商户后台 除了 API,ST Pay 还提供商户后台,供您查询订单、核对余额与管理员工账号。后台不能创建订单,也不能导出数据;创建订单一律通过 API。 ## 登录 | 项目 | 说明 | | --- | --- | | 网址 | 由 ST Pay 对接窗口提供(与 API 的域名不同);测试环境与生产环境的域名不同,生产环境的域名在测试环境完成对接后另行提供。两个环境的账号相互独立。 | | 登录信息 | 账号、密码、4 位数字图形验证码。看不清验证码时,点击图片换一张。 | | 来源 IP | 只能从 ST Pay 为您设置的**后台登录白名单** IP 登录(与 API 白名单是两份不同的名单)。白名单为空时无法登录。需要新增或变更时,请联系您的 ST Pay 对接窗口。 | | 第一个账号 | 由 ST Pay 创建商户主管账号并交付初始密码。 | | 密码 | 至少 6 个字符,须包含英文字母与数字,不得与账号相同。初始密码与被重置的密码,首次登录时必须先修改,不能跳过,修改前无法使用其他功能;新密码不得与当前密码相同。之后可从右上角的账号菜单“修改密码”。 | | 锁定 | 密码连续错误 5 次,账号会被锁定(验证码错误不计次;修改密码时旧密码输入错误也计入)。商户员工由商户主管解除,商户主管请联系 ST Pay。 | | 忘记密码 | 没有自助重置。商户员工由商户主管重置,商户主管请联系 ST Pay。 | | 自动退出 | 闲置 30 分钟自动退出;登录满 12 小时须重新登录;账号被停用时立即退出;来源 IP 被移出后台登录白名单(或换到白名单外的 IP)时也立即退出。 | | 语言 | 右上角可切换繁體中文、简体中文、English。 | 商户被 ST Pay 停用时,只是不能再创建订单;仍可登录后台,也可以通过 API 查单。 ## 角色与页面 | 页面 | 商户主管 | 商户员工 | 内容 | | --- | --- | --- | --- | | 订单查询 | 可用 | 可用 | 查询本商户的代收单与代付单 | | 余额流水 | 可用 | 不可用 | 当前可用余额与每笔余额变动 | | 员工账号 | 可用 | 不可用 | 创建与管理商户员工账号 | 商户主管可以再关闭个别员工的页面或“显示完整账号”权限(见[员工账号](#员工账号))。 ## 订单查询 * 分“代收单”“代付单”两个标签页,最新的订单排在最前面,一次最多列出最新的 500 笔。 * 可按状态、创建时间、订单号(完整)、商户单号、通道类别筛选;创建时间默认为当日(00:00:00 至 23:59:59),可自行调整或清空,点击“重置”回到当日。页面不会自动刷新,请点击“刷新”或“查询”。 * 只列出保存期限(6 个月)内的订单。 * 状态的显示文字与 API 的 `status` 对应如下(另见[订单状态](../reference/status.md)): | `status` | 后台显示 | | --- | --- | | `pending` | 待审批 | | `success` | 成功 | | `rejected` | 已拒绝 | | `late_credited` | 超时入账 | | `paid` | 已代付 | * 字段名称与 API 相同:订单金额 `payable_amount`、原始金额 `amount`、实收金额 `received_amount`、手续费 `fee`、结算金额 `settlement_amount`、代付金额 `amount`、实扣总额 `total_deducted`。 * **付款人姓名只在后台显示**,查单 API 与 Callback 都不会返回。 * 账号默认脱敏(只显示末 4 位)。代收单的收款账号可以双击显示完整账号,一次只显示一笔,且 ST Pay 会留下记录;代付收款人账号在商户后台只显示脱敏后的值。 * 点击“详情”可看到拒绝原因与 **回调发送记录**:每次发送的时间、结果(成功或失败)与触发方式(自动或手动)。送达失败不会自动重试,需要重发时请联系 ST Pay(见 [Callback](../api/callback.md))。 ## 余额流水 顶部显示当前的可用余额,下方按时间由新到旧列出每笔变动,一次最多列出最新的 1000 笔;可按时间与变动类型筛选,时间默认为当日。流水不能修改或删除。 | 变动类型 | 何时产生 | 方向 | | --- | --- | --- | | 代收入账 | 代收单入账成功(`success`),金额为结算金额 | 增加 | | 超时入账 | 代收单转为 `late_credited`,金额为结算金额。**这种情况不发送 Callback**,请以此核对 | 增加 | | 代付出款 | 代付单出款(`paid`),金额为实扣总额 | 扣减 | | 人工增加、人工扣减 | ST Pay 人工调整余额;“原因/备注”栏是 ST Pay 填写的说明 | 增加或扣减 | 每笔都有调整前余额、调整后余额与关联订单号,可用来与您的账务逐笔核对。 ## 员工账号 商户主管可以管理本商户的商户员工账号: | 操作 | 说明 | | --- | --- | | 创建账号 | 输入账号与初始密码(规则同上),员工首次登录时必须修改密码。账号只能使用英文字母、数字与 `.` `_` `-`,以字母或数字开头,最多 64 个字符。 | | 停用 | 停用后无法登录,已登录的连接立即中断,**不能再改回正常**。 | | 解除锁定 | 员工密码连续错误 5 次被锁定时使用。 | | 重置密码 | 输入新的初始密码,原密码立即失效,员工下次登录时必须修改。 | | 权限 | 可关闭该员工的“订单查询”页面,或“显示完整账号”操作,保存后立即生效。 | 商户主管账号由 ST Pay 创建、停用、解除锁定与重置密码。由 ST Pay 创建的账号,列表的创建人显示“ST Pay”。 --- --- url: /zh-cn/guide/conventions.md --- # 请求与响应格式 ## 请求 * 统一使用 HTTPS 与 `POST`,`Content-Type: application/json`,请求体使用 UTF-8 编码。 * 请求体为一个 JSON 对象。**所有字段值都必须是 JSON 字符串**,金额也不例外,例如 `"amount": "1000.00"`;传数字 `1000.00` 会失败(返回 `invalid_signature` 或 `invalid_parameter`)。 * 每个请求都必须携带 `api_identity`、`timestamp` 与 `sign`(见[签名](./signature.md))。 * `timestamp` 是发送请求当下的时间,格式同下方的[时间](#数据格式)(UTC+8,`yyyy-MM-dd HH:mm:ss`)。与 ST Pay 服务器时间相差超过 5 分钟(过去或未来都算)的请求会被拒绝,返回 `timestamp_expired`。请为服务器开启 NTP 时间同步。 * 选填字段传空字符串 `""`,视同未传。 ## 响应 所有 API 都返回相同的结构: | 字段 | 说明 | | --- | --- | | `code` | 成功为 `OK`,失败为英文错误码(见[错误码](../reference/errors.md))。**程序请以此字段判断结果。** | | `message` | 繁体中文说明,供人工阅读,内容可能调整,请勿用于判断。 | | `data` | 成功时为返回数据,失败时为 `null`。 | HTTP 状态码:成功为 `200`,失败时按错误类型返回 4xx 或 5xx(见[错误码](../reference/errors.md))。 ```json { "code": "merchant_order_no_duplicate", "message": "商戶單號重複", "data": null } ``` ## 数据格式 | 类型 | 格式 | | --- | --- | | 金额 | 字符串。请求:大于 0、最多 2 位小数(按字面计算,`"1000.500"` 是 3 位)、不超过 `9999999999999999.99`,只能包含数字与小数点,例如 `"1000"`、`"1000.5"`、`"1000.50"`。`"1,000.00"`、`"1000.001"`、`"-5"`、`"0"` 均不合法。响应与 Callback 中的金额统一为 2 位小数,例如 `"1000.50"`。不包含币种字段。 | | 时间 | 字符串,UTC+8,格式为 `yyyy-MM-dd HH:mm:ss`,例如 `"2026-10-13 10:30:05"`。 | | 订单号 `order_no` | ST Pay 生成的字符串。请作为不透明字符串保存,不要解析其内容或依赖其格式。 | | 商户单号 `merchant_order_no` | 由您生成,1 至 200 个字符,只能包含英文字母、数字、下划线 `_`、连字符 `-`。 | ## 数据保存期限 订单自创建起保存 6 个月,之后会被删除。删除后查单会返回 `order_not_found`,该商户单号也可以再次使用。 --- --- url: /zh-cn/guide/security.md --- # 身份验证与 IP 白名单 ## 检查顺序 每个请求按以下顺序检查,任一项不通过即返回错误: 1. **签名**:`api_identity` 必须是状态为正常的凭证,且 `sign` 正确。不通过时返回 `invalid_signature`(HTTP 401)。 2. **时间戳**:`timestamp` 必填,格式 `yyyy-MM-dd HH:mm:ss`(UTC+8),与 ST Pay 服务器时间相差不得超过 5 分钟。缺少时返回 `missing_parameter`,格式不符返回 `invalid_parameter`,相差超过 5 分钟返回 `timestamp_expired`(HTTP 400)。签名通过后才检查时间戳,因此 `timestamp` 被篡改时返回 `invalid_signature`。 3. **来源 IP**:必须在该商户的 API 白名单内。白名单为空时全部拒绝。不通过时返回 `ip_not_allowed`(HTTP 403)。 4. **商户状态**(仅建单时检查):商户被停用时不能建单,返回 `merchant_disabled`(HTTP 403)。停用期间仍可查单。 ## 保管密钥 * 密钥只能存放在您的服务器端,不要放入网页、App 或任何会分发给终端用户的程序中。 * 密钥泄露或丢失时,请立即联系 ST Pay 创建新凭证并停用旧凭证。 ## 更换密钥 同一商户可以同时拥有多组状态为正常的凭证,每组都可用于调用 API。Callback 统一使用**最新创建且状态为正常**的那组密钥签名,因此建议按以下顺序更换密钥: 1. 先让您的 Callback 验签程序能同时接受两个密钥(新密钥要在第 2 步之后才能拿到,可先预留好配置位置)。 2. 请 ST Pay 创建新凭证,并将新密钥加入 Callback 验签。新凭证创建后,Callback 会立即改用新密钥签名;新密钥加入验签之前收到的 Callback 会验签失败,这些订单请以[查单](../api/query.md)确认结果,或请 ST Pay 重发。 3. 调用 API 时改用新的 `api_identity` 与密钥。 4. 确认已没有请求使用旧凭证后,请 ST Pay 停用旧凭证,并从 Callback 验签中移除旧密钥。 --- --- url: /zh-cn/guide/signature.md --- # 签名 API 请求、API 响应与 Callback 使用同一套签名规则(HMAC-SHA256)。 ## 计算步骤 1. 取请求体中除 `sign` 以外的**所有**字段,排除值为 `null` 的字段。空字符串 `""` 不排除。 2. 按**字段名**的字符编码(ASCII)从小到大排序。 3. 每个字段写成 `字段名=字段值`,用 `&` 拼接成一个字符串(以下称为“待签名字符串”)。字段值按原样放入,**不做 URL 编码**,也不去除空白字符。 4. 以 API 验证密钥为 key,对待签名字符串的 UTF-8 字节计算 HMAC-SHA256。 5. 将结果转换为**小写**十六进制字符串(64 个字符),放入 `sign` 字段。 ::: warning 常见错误 * 请直接将密钥以 UTF-8 字符串作为 key,**不要**做 Base64 解码(密钥看起来像 Base64,但不需要解码)。 * 参与签名的值必须与 JSON 中实际发送的值完全相同。例如签名时用的是 `1000.00`,发送的却是 `"1000"`,签名就会不一致。 * 不要用 `http_build_query`、`URLSearchParams`、`urlencode` 之类的函数拼接字符串,它们会做 URL 编码。 * 中文字段值(例如 `payer_name`)请以 UTF-8 计算,不要使用 GBK、Big5 等其他编码。 * 签名覆盖请求体中的所有字段,包括本文档未列出的字段。请不要在签名之后再添加字段。 * `callback_url` 也要参与签名,按原样放入待签字符串;即使网址的 query 含 `?`、`&`、`=` 也不要编码。 * `timestamp` 也要参与签名。请在发送请求当下取时间再签名,不要重复使用旧的请求;与 ST Pay 服务器时间相差超过 5 分钟会被拒绝(`timestamp_expired`)。 ::: ## 示例 以下使用示例凭证,您可以用它验证自己的程序是否计算出相同的结果(`timestamp` 为示意值,实际调用请使用当下时间): | 项目 | 示例值 | | --- | --- | | `api_identity` | `qp_Xk3vT9aLm2QwE7rB1nZc` | | API 验证密钥 | `Rk8vN2qXw5LpT9sJc3HmY7bF1dGz6AeU4oKi0Wn-QtE` | 要发送的字段(签名前): ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "merchant_user_id": "U10086", "payer_name": "王小明", "amount": "1000.00", "channel_type": "fps", "response_mode": "url", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:00:00" } ``` 排序并拼接后的待签名字符串: ```text amount=1000.00&api_identity=qp_Xk3vT9aLm2QwE7rB1nZc&callback_url=https://shop.example.com/qpay/callback&channel_type=fps&merchant_order_no=MO20261013-0001&merchant_user_id=U10086&payer_name=王小明&response_mode=url×tamp=2026-10-13 10:00:00 ``` HMAC-SHA256 结果(即 `sign`): ```text 566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2 ``` ## 代码示例 以下示例均已使用上面的示例凭证验证,计算出的签名与上方结果一致。验证 Callback 时,请用收到的全部字段重新计算签名,再进行恒定时间比较。 ::: code-tabs @tab Python 3 ```python import hmac, hashlib def qpay_sign(fields: dict, secret: str) -> str: items = sorted((k, v) for k, v in fields.items() if k != "sign" and v is not None) text = "&".join(f"{k}={v}" for k, v in items) return hmac.new(secret.encode("utf-8"), text.encode("utf-8"), hashlib.sha256).hexdigest() # 验证 Callback def qpay_verify(fields: dict, secret: str) -> bool: return hmac.compare_digest(qpay_sign(fields, secret), str(fields.get("sign", "")).lower()) ``` @tab Java 17+ ```java import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.HexFormat; import java.util.Map; import java.util.TreeMap; import java.util.stream.Collectors; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public final class QpaySigner { public static String sign(Map fields, String secret) throws Exception { String text = new TreeMap<>(fields).entrySet().stream() .filter(e -> !"sign".equals(e.getKey()) && e.getValue() != null) .map(e -> e.getKey() + "=" + e.getValue()) .collect(Collectors.joining("&")); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); return HexFormat.of().formatHex(mac.doFinal(text.getBytes(StandardCharsets.UTF_8))); } /** 验证 Callback */ public static boolean verify(Map fields, String secret) throws Exception { String sign = fields.getOrDefault("sign", ""); return MessageDigest.isEqual(sign(fields, secret).getBytes(StandardCharsets.UTF_8), sign.toLowerCase().getBytes(StandardCharsets.UTF_8)); } } ``` @tab PHP 7.4+ ```php $v) { $parts[] = $k . '=' . $v; // 不要用 http_build_query,它会做 URL 编码 } return hash_hmac('sha256', implode('&', $parts), $secret); } // 验证 Callback function qpay_verify(array $fields, string $secret): bool { return hash_equals(qpay_sign($fields, $secret), strtolower((string) ($fields['sign'] ?? ''))); } ``` @tab Node.js ```js const crypto = require('crypto'); function qpaySign(fields, secret) { const text = Object.keys(fields) .filter((k) => k !== 'sign' && fields[k] !== null && fields[k] !== undefined) .sort() .map((k) => `${k}=${fields[k]}`) .join('&'); return crypto.createHmac('sha256', secret).update(text, 'utf8').digest('hex'); } // 验证 Callback function qpayVerify(fields, secret) { const expected = Buffer.from(qpaySign(fields, secret)); const actual = Buffer.from(String(fields.sign || '').toLowerCase()); return expected.length === actual.length && crypto.timingSafeEqual(expected, actual); } ``` @tab Go ```go import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "sort" "strings" ) // 解析响应或 Callback 的 JSON 时也用 map[string]any:值为 null 的字段会是 nil,不纳入签名 func QpaySign(fields map[string]any, secret string) string { keys := make([]string, 0, len(fields)) for k, v := range fields { if k != "sign" && v != nil { keys = append(keys, k) } } sort.Strings(keys) parts := make([]string, len(keys)) for i, k := range keys { parts[i] = k + "=" + fields[k].(string) } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(strings.Join(parts, "&"))) return hex.EncodeToString(mac.Sum(nil)) } // 验证 Callback func QpayVerify(fields map[string]any, secret string) bool { sign, _ := fields["sign"].(string) return hmac.Equal([]byte(QpaySign(fields, secret)), []byte(strings.ToLower(sign))) } ``` ::: ## 验证响应 成功响应(`code` 为 `OK`)的 `data` 带有 `sign`,ST Pay 以**本次请求使用的 API 验证密钥**计算,规则同上:取 `data` 中 `sign` 以外、值不为 `null` 的所有字段。 * 请先验签,通过后才使用响应内容,特别是代收建单返回的 `account_no`(付款人要转入的账号)与 `payment_url`。 * 验签不通过表示响应可能在传输途中被篡改:不要把账号或网址交给付款人,改以[查单](../api/query.md)确认。 * 验签可直接使用上面的验证函数,传入响应的 `data`。 * 错误响应的 `data` 为 `null`,不带签名。 ## 完整调用示例 ```bash curl -X POST 'https://merchant.vincoterra.com/api/v1/collect/orders' \ -H 'Content-Type: application/json' \ -d '{ "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "merchant_user_id": "U10086", "payer_name": "王小明", "amount": "1000.00", "channel_type": "fps", "response_mode": "url", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:00:00", "sign": "566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2" }' ``` --- --- url: /zh-cn/reference/banks.md --- # 银行代码 代付银行转账的 `bank_code` 必须是下列值之一。列表由 ST Pay 维护,如有变动会另行通知。 | `bank_code` | 银行名称 | | --- | --- | | `003` | Standard Chartered Bank (Hong Kong) Limited | | `004` | The Hongkong and Shanghai Banking Corporation Limited | | `009` | China Construction Bank (Asia) Corporation Limited | | `012` | Bank of China (Hong Kong) Limited | | `015` | The Bank of East Asia, Limited | | `016` | DBS Bank (Hong Kong) Limited | | `018` | China CITIC Bank International Limited | | `020` | CMB Wing Lung Bank Limited | | `024` | Hang Seng Bank Limited | | `025` | Shanghai Commercial Bank Limited | | `027` | Bank of Communications Co., Ltd. Hong Kong Branch | | `028` | Public Bank (Hong Kong) Limited | | `035` | OCBC Bank (Hong Kong) Limited | | `039` | Chiyu Banking Corporation Limited | | `040` | Dah Sing Bank, Limited | | `041` | Chong Hing Bank Limited | | `043` | Nanyang Commercial Bank, Limited | | `072` | Industrial and Commercial Bank of China (Asia) Limited | | `128` | Fubon Bank (Hong Kong) Limited | | `250` | Citibank (Hong Kong) Limited | | `382` | Bank of Communications (Hong Kong) Limited | | `387` | ZA Bank Limited | | `388` | Livi Bank Limited | | `389` | Mox Bank Limited | | `390` | WeLab Bank Limited | | `391` | Fusion Bank Limited | | `392` | PAObank Limited | | `393` | Ant Bank (Hong Kong) Limited | | `395` | Airstar Bank Limited | --- --- url: /zh-cn/reference/checklist.md --- # 上线前检查清单 1. 用[示例凭证](../guide/signature.md#示例)计算出的签名与文档一致。 2. 所有字段值都以字符串发送,金额不是数字类型。 3. 代收建单的 url 与 json 两种模式(如有使用)、fps 与 bank_transfer 两种通道都已测试。 4. 付款页面显示的是 `payable_amount`,而不是原始金额。 5. 代付建单的 FPS 三种账号格式(如有使用)与银行转账都已测试。 6. Callback 接口:验签正确、签名不一致时会丢弃、重复通知能幂等处理、10 秒内返回 `ok`。 7. Callback 以 `order_no` 匹配订单,能区分代收与代付。 8. 有定期查单机制,能处理未收到 Callback 与 `late_credited` 的情况。 9. 建单超时与 `internal_error` 会以商户单号查单确认,不会直接判定为失败。 10. 程序以 `code` 判断结果,不依赖 `message` 文本。 11. 生产环境的凭证、域名与 IP 白名单已另行申请,且密钥只存放在服务器端。 12. 服务器时间已校准(建议开启 NTP 时间同步),每个请求的 `timestamp` 都是发送当下的时间。 13. 每个建单请求都传入 `callback_url`;地址是合法的 `http` 或 `https` 地址(两者都接受),域名可从公网解析且只指向公网 IP,不会重定向(3xx),服务器返回 HTTP 2xx。 14. 代收成功以 `received_amount`(或 `settlement_amount`)入账;与预期金额不同时转人工,不自动入账。 15. 已是终态的订单不会因后到、状态不同的 Callback 被回退;有疑义以[查单](../api/query.md)结果为准。 --- --- url: /zh-cn/reference/errors.md --- # 错误码 建单时,ST Pay 按以下顺序检查,同时违反多项时只返回第一个错误: 签名 → 时间戳 → 来源 IP → 商户状态 → 必填字段与格式 → `callback_url` 的环境与地址 → 单笔上下限 → 商户单号重复 →(代收)同一会员待审批订单上限 →(代收)收款账户与尾数分配 /(代付)余额。 在此之前:请求体不是 JSON 对象或 `Content-Type` 不是 `application/json`,直接返回 `invalid_parameter`;未传 `api_identity` 或 `sign` 返回 `invalid_signature`。 `message` 实际返回繁体中文,程序请以 `code` 判断。下表 `message` 列为系统返回的原文。 | `code` | HTTP | `message` | 原因与处理方式 | | --- | --- | --- | --- | | `invalid_signature` | 401 | 簽章驗證失敗 | `api_identity` 错误、凭证已停用,或签名不一致。请对照[签名](../guide/signature.md#示例)中的示例核对程序,并确认使用了对应环境的凭证。字段值传成数字或布尔值会返回此错误或 `invalid_parameter`,所有值请以字符串传送;未传 `api_identity` 或 `sign` 也是。 | | `timestamp_expired` | 400 | 請求時間與伺服器時間相差超過 5 分鐘,請校正您的伺服器時間(伺服器目前時間:yyyy-MM-dd HH:mm:ss) | `timestamp` 与 ST Pay 服务器时间相差超过 5 分钟(过去或未来都算)。消息中附有服务器当前时间,可据此判断差距;请开启 NTP 校准服务器时间,并在发送当下才取时间签名。 | | `ip_not_allowed` | 403 | 來源 IP 不在白名單 | 请确认服务器实际的出口 IP,并请 ST Pay 将其加入白名单;加入后约 1 分钟生效。来源 IP 不在任何白名单时,请求在网关就被拦下:返回 HTTP 403,响应体是 HTML 而不是 JSON,处理方式相同。 | | `merchant_disabled` | 403 | 商戶已停用 | 商户停用期间不能建单,请联系 ST Pay。 | | `permission_denied` | 403 | 無此操作權限 | 通常是 `/api/v1/` 下的路径拼写错误。使用 `GET`,或路径不在 `/api/v1/` 下时,网关直接返回 HTTP 404(响应体不是 JSON)。 | | `missing_parameter` | 400 | 缺少必填欄位 | 必填字段未传、为 `null` 或空字符串;`callback_url` 全是空白;银行转账代付缺少银行字段;查单未传任何单号。 | | `invalid_parameter` | 400 | 欄位值不合法 | 字段值的格式、长度、字符或枚举值不合法;字段值不是字符串;请求体不是合法的 JSON 对象,或 `Content-Type` 不是 `application/json`;`callback_url` 格式不符(规则见 [Callback 地址](../api/callback.md#callback-地址));FPS 代付传了银行字段;银行代码不在列表内。 | | `invalid_amount` | 400 | 金額格式不正確 | 金额为 0、负数、超过 2 位小数(按字面计算,`1000.500` 也算 3 位)、大于 `9999999999999999.99`,或不是数字。 | | `missing_member_identity` | 400 | 請提供會員唯一識別或真實姓名 | 代收建单时 `merchant_user_id` 与 `payer_name` 均未传。 | | `invalid_callback_url` | 400 | Callback 網址不符合規定:不可為本機或內網位址 | 建单的 `callback_url` 主机是本机、内网地址、内部域名、非标准的 IP 写法。见 [Callback 地址](../api/callback.md#callback-地址)。格式错误(不是网址、超过 500 个字符、含空白或其他格式规则,见同一节)返回 `invalid_parameter`。 | | `amount_out_of_range` | 422 | 金額超出限額 | 原始金额或代付金额超出单笔上下限。 | | `merchant_order_no_duplicate` | 409 | 商戶單號重複 | 见[重复请求与结果确认](./idempotency.md)。 | | `user_active_orders_limit` | 422 | 有效訂單已達上限 | 同一会员已有 10 笔待审批的代收单。请等待其中的订单完成或到期后再创建。 | | `channel_full` | 422 | 通道已滿,請輸入其他金額再試 | 同一通道、同一原始金额的识别尾数已全部被待审批订单占用。请让付款人换一个金额再试。 | | `no_available_card` | 422 | 通道已滿,請輸入其他金額再試 | 该通道当前没有可用的收款账户。可稍后再试或改用另一个通道;持续出现请联系 ST Pay。 | | `insufficient_balance` | 422 | 商戶餘額不足 | 代付建单时,可用余额小于代付金额加手续费。 | | `order_not_found` | 404 | 查無此單 | 订单不存在、不属于您,或已超过保存期限被删除。 | | `query_conflict` | 400 | 訂單號與商戶單號指向不同訂單 | 查单时同时传了两个单号,但它们不是同一笔订单。 | | `internal_error` | 500 | 系統錯誤,請以商戶單號查單確認結果 | ST Pay 内部错误,订单可能已创建也可能未创建。**不要直接视为失败**,请以商户单号查单确认。 | `channel_full` 与 `no_available_card` 的 `message` 相同,请以 `code` 区分。 收到 `invalid_parameter`、`invalid_signature` 等错误又找不出原因时,请提供 `api_identity`、商户单号(查单则提供查询的单号)与发生时间(UTC+8),请 ST Pay 对接联系人查询 API 错误记录,可查到是哪个字段有问题或签名失败的原因。ST Pay 只记录 `api_identity` 正确的请求;`api_identity` 本身错误时查不到,请先核对。 --- --- url: /zh-cn/reference/idempotency.md --- # 重复请求与结果确认 | 情况 | 结果与建议做法 | | --- | --- | | 使用已用过的商户单号再次建单 | 返回 `merchant_order_no_duplicate`,无论原订单状态如何,也不比对内容(金额不同也一样)。不会生成第二笔订单。响应不附带原订单数据,请用商户单号[查单](../api/query.md)。 | | 建单请求超时、连接中断,或收到 `internal_error` | **不要直接视为失败**。用同一个商户单号查单:查得到则以查单结果为准(待付款且尚未到失效时间的订单,查单会附上付款网址或收款账号,可以继续让付款人支付该订单,见[查单](../api/query.md));返回 `order_not_found` 表示 ST Pay 没有创建该订单,可以用同一个商户单号重新建单。 | | 订单被拒绝后要重新发起 | 必须使用新的商户单号。 | | 商户单号的唯一性范围 | 同一商户内,代收单与代付单各自不能重复;订单超过 6 个月被删除后,该单号可以再次使用。 | | 重复调用查单 | 只读,每次返回当前状态,不会产生任何变更。 | | 几乎同时提交多个相同商户单号的建单请求 | 依次处理,只有第一个成功,其余返回 `merchant_order_no_duplicate`。 | | 同一笔订单收到多次 Callback | 以 `order_no` 加 `status` 判断是否已处理过,重复的直接返回 `ok`。 | --- --- url: /zh-cn/reference/status.md --- # 订单状态与金额 “后台显示”是[商户后台](../guide/backoffice.md)显示的状态文字。 ## 代收单 ```text pending ──┬── 确认入账 ───────────────→ success (发送 Callback) ├── ST Pay 人工拒绝 ─────────→ rejected (发送 Callback,终态) └── 30 分钟到期,系统拒绝 ──→ rejected (发送 Callback) │ └── 之后确认收到款项 ──→ late_credited (不发送 Callback) ``` | `status` | 后台显示 | 含义 | 是否为最终结果 | | --- | --- | --- | --- | | `pending` | 待审批 | 等待付款人转账与 ST Pay 确认 | 否 | | `success` | 成功 | 入账成功,结算金额已计入您的余额 | 是 | | `rejected` | 已拒绝 | 不入账、不收手续费 | 人工拒绝是最终结果;**系统超时拒绝之后仍可能变为 `late_credited`** | | `late_credited` | 超时入账 | 订单已被系统超时拒绝,但之后确认付款人确实已付款,结算金额已计入您的余额 | 是 | ::: warning 超时入账不发送 Callback 您会先收到一次 `rejected` 的 Callback,之后同一笔订单若转为 `late_credited`,不会再收到通知,但余额已经增加。 建议:付款人反馈已付款但订单显示已拒绝时,请通过查单确认;并定期通过查单或[商户后台的余额流水](../guide/backoffice.md#余额流水)(变动类型“超时入账”)对账,核对 `reject_reason` 为“訂單已逾時”(系统返回的繁体中文原文)的订单是否已转为 `late_credited`。 ::: ## 代付单 ```text pending ──┬── ST Pay 出款 ──→ paid (扣款并发送 Callback) └── ST Pay 拒绝 ──→ rejected (不扣款并发送 Callback) ``` | `status` | 后台显示 | 含义 | 是否为最终结果 | | --- | --- | --- | --- | | `pending` | 待审批 | 等待 ST Pay 出款 | 否 | | `paid` | 已代付 | 已出款,已从余额扣除代付金额与手续费 | 是 | | `rejected` | 已拒绝 | 不扣款、不收手续费 | 是 | 代付单没有自动超时机制,会保持 `pending` 直到 ST Pay 处理。 ## 金额与手续费 | 项目 | 计算方式 | | --- | --- | | 代收订单金额 `payable_amount` | 原始金额 + 识别尾数(0.01–0.99) | | 代收手续费 `fee` | 实收金额 × 代收费率,保留 2 位小数,第 3 位起直接舍去(不四舍五入) | | 代收结算金额 `settlement_amount` | 实收金额 − 手续费 | | 代付手续费 `fee` | 代付金额 × 代付费率,保留 2 位小数,第 3 位起直接舍去(不四舍五入) | | 代付实扣总额 `total_deducted` | 代付金额 + 手续费 | * 费率在**建单时锁定**,之后的费率调整只影响新创建的订单。 * 实收金额 `received_amount` 是 ST Pay 实际确认入账的金额,手续费与结算金额均以此计算。若付款人转账金额与订单金额不同,实收金额可能与 `payable_amount` 不一致。 * 被拒绝的订单手续费为 `0.00`。 * 每笔订单的原始金额或代付金额须在 ST Pay 设定的单笔上下限范围内(含上下限本身)。上下限按“代收或代付”与“通道类型”分别设定,请向 ST Pay 确认当前的数值。 示例:代收费率 2.5%,实收 1000.37,手续费为 1000.37 × 2.5% = 25.00925,舍去后为 25.00,结算金额为 975.37。