如何签名与验签
国际版 API 使用一套无 Token 的服务端签名机制:普通 API 请求由商户签给接口服务;普通 API 响应由接口服务签给商户;Webhook 通知请求由平台签给商户。三类场景的密钥、请求方向和 参与签名字段不同,必须分别实现。
商户管理员或明确授权的技术人员在商户 Portal 的“国际支付接入 → API 接入”中创建 API 凭据。API Secret 由平台安全生成,创建成功后只在当前页面会话中展示一次;平台不接受自定义 Secret。刷新或关闭浏览器后,商户、PMS 和技术支持都不能再次查看或找回该 Secret。商户应立即保存到 服务端 Vault、KMS 或受控环境变量。
常规轮换采用“创建替代 Key → 新旧 Key 并行 → 用 last_used_at 确认切换 → 停用旧 Key 观察 → 确认无流量后吊销旧 Key”。如果操作人无法取得当前 Secret,或怀疑 Secret 已泄露, 都必须创建替代 Key 并吊销旧 Key。
API 请求如何签名
普通 API 请求是商户后端调用接口服务。商户使用 API Secret 对本次 HTTP 请求生成 HMAC-SHA256 签名,并把 API Key、签名时间、随机串和签名值放入独立请求头。
1. 准备签名材料
| 材料 | 说明 |
|---|---|
| API Key | 商户后台获取的接口调用凭据,用于识别商户、密钥和权限;调用来源由平台运营侧安全组控制。 |
| API Secret | 与 API Key 对应的服务端密钥,只用于本地计算签名,不能发送给接口服务。 |
| HTTP 请求方法 | 使用大写方法,例如 POST、GET。 |
| 请求 URL | 去掉域名后的 Path 和原始 Query,例如 /intl/v1/payment/order/query?merchantOrderNo=DEMO_PAY_202606010001。 |
| 请求时间戳 | Unix 秒级时间戳文本,通常允许与接口服务端时间相差 ±300 秒。 |
| 请求随机串 | Nonce 的值,也可称为 nonce;同一 API Key 下每次请求必须唯一,最长 128 字符。 |
| 请求报文主体 | 最终发送到网络上的 UTF-8 原始 Body;GET 没有 Body 时使用空字符串。 |
2. 获取 HTTP 请求方法
取实际调用接口使用的 HTTP 方法,并转成大写:
POST3. 获取请求 URL
请求 URL 只包含 Path 和 Query,不包含协议、域名、端口和 fragment。Query 必须使用客户端最终实际 发送的原始顺序和编码,不要重新排序、解码或二次编码。
/intl/v1/payment/order/create带 Query 的查询接口示例:
/intl/v1/payment/order/query?merchantOrderNo=DEMO_PAY_202606010001余额查询的请求 URL 按实际筛选条件生成;不传 country 和 currency 时,签名使用:
/intl/v1/balance/query同时传国家和币种筛选时,签名使用实际 Query:
/intl/v1/balance/query?country=PH¤cy=PHP4. 生成请求时间戳
时间戳使用 Unix 秒,并作为 Header 文本发送。商户服务器必须通过 NTP 等方式保持时间准确;时间偏差过大时接口服务会在业务处理前拒绝请求。
date +%s示例值:
17802720005. 生成请求随机串
Nonce 用于降低重放风险。同一 API Key 下不要复用随机串;重试也必须重新生成随机串。
openssl rand -hex 16示例值:
nonce_fixture_0016. 获取请求报文主体
POST 请求使用最终发送的原始 JSON 字符串参与签名。签名前后不得改变空格、换行、字段顺序或字符 编码;如果框架会重新序列化 JSON,必须使用真正发出的字节内容签名。
GET 请求没有请求体,第五行为空字符串,但仍然要保留最后一个换行字节。
7. 构造请求签名原文
请求签名原文固定五行,每一行后都必须追加换行字节 \n,包括最后一行。如果某个参数本身以 \n 结尾,也需要额外追加本规则要求的 \n。
HTTP请求方法\n
请求URL\n
请求时间戳文本\n
请求随机串\n
请求报文主体\n示例:
POST\n
/intl/v1/payment/order/create\n
1780272000\n
nonce_fixture_001\n
{"merchantOrderNo":"DEMO_PAY_202606010001","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","amount":{"value":"100.50"},"orderDescription":"Virtual order"}\n8. 计算签名值
使用 API Secret 对签名原文计算 HMAC-SHA256,并转成小写十六进制字符串,再加上 v1= 前缀。
printf 'POST\n/intl/v1/payment/order/create\n1780272000\nnonce_fixture_001\n{"merchantOrderNo":"DEMO_PAY_202606010001","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","amount":{"value":"100.50"},"orderDescription":"Virtual order"}\n' \
| openssl dgst -sha256 -hmac "$INTL_API_SECRET" -hex最终签名格式:
v1=<64位小写十六进制>9. 组装请求签名 Header
Api-Key: ik_live_xxx
Timestamp: 1780272000
Nonce: nonce_fixture_001
Signature: v1=<64位小写十六进制>注意:
- 请求 Header 中不包含 API Secret。
Timestamp、Nonce和Signature必须与参与签名的值完全一致。- 普通 API 请求还必须发送标准 HTTP 格式 Header:
Accept: application/json;POST 请求还必须发送Content-Type: application/json。这两个 Header 不参与签名原文。 - Header 名称大小写不敏感;文档统一展示无品牌前缀的规范 Header 名,调用时不要自行添加产品、公司或品牌前缀。
- 请求和响应报文格式见基本规则。
- 推荐优先使用 SDK,自研实现必须在上线前通过平台提供的请求签名测试向量。
API 响应如何验签
普通 API 响应是接口服务返回给商户。接口服务会用本次请求 API Key 对应的 API Secret 对响应原始 Body 生成签名,商户收到响应后应先验签,再按 JSON 解析和处理业务状态。
1. 获取响应头
普通 API 响应头包含以下字段:
| Header | 返回时机 | 说明 |
|---|---|---|
Content-Type | 所有带 JSON Body 的普通 API 响应 | 固定为 application/json,表示响应 Body 是 UTF-8 JSON;不参与响应验签原文。 |
Request-Id | 所有普通 API 响应 | 请求追踪号,用于报障、日志关联和对账。 |
Timestamp | 可识别 API Key 的普通 API 响应 | 响应签名时间,Unix 秒级时间戳文本。 |
Nonce | 可识别 API Key 的普通 API 响应 | 响应随机串,本次响应唯一。 |
Signature | 可识别 API Key 的普通 API 响应 | v1= 加小写 HMAC-SHA256 十六进制响应签名。 |
如果接口服务无法识别 API Key,例如 Api-Key 缺失或格式严重错误,认证失败响应可能无法生成 响应签名。此时商户仍应保存 Request-Id、HTTP 状态码和错误码用于排障。
2. 检查时间戳
检查 Timestamp 与商户服务器当前时间的偏差。默认建议允许最大 300 秒偏差;超过偏差时应拒绝把该 响应作为可信结果处理,并记录 Request-Id 排查网络代理或时钟问题。响应签名算法固定为 HMAC-SHA256,签名前缀固定为 v1=。
3. 获取响应原始 Body
验签必须使用原始 Response Body。不要使用已经格式化、转义、重新排序字段或重新序列化后的 JSON 字符串。若响应 Body 为空,第三行使用空字符串,但仍然保留最后一个换行字节。
4. 构造响应验签原文
响应验签原文固定三行,每一行后都必须追加换行字节 \n,包括最后一行:
响应时间戳文本\n
响应随机串\n
响应报文主体\n示例:
1780272000\n
nonce_fixture_response_001\n
{"platOrderNo":"PHI112606010000000001001","status":"PROCESSING"}\n5. 计算并比对签名
商户使用本次请求 API Key 对应的 API Secret,对响应验签原文计算 HMAC-SHA256,小写十六进制后加 v1= 前缀,并与响应头 Signature 做常量时间比较。
验签不通过时:
- 不要按响应 Body 更新业务最终状态。
- 记录环境、时间、商户订单号、
Request-Id响应 Header、HTTP 状态码和响应 Bodyerror.code。 - 收到已验签的 HTTP
5xx创建响应,或响应无法验签时,都不能直接确认订单是否已经创建; 必须先按原merchantOrderNo查单。连续查询一段时间仍不存在时,才能用同一单号和完全一致的原请求重发。 - 排查代理、网关或框架是否丢失响应头、修改 Body、自动解压或重编码。
Webhook 响应如何验签
Webhook 是平台主动调用商户通知地址的请求。这里的“验签”是商户验证平台发来的 Webhook 请求;商户对平台的 HTTP 应答不需要签名。
1. 获取 Webhook 请求头
Webhook 请求头使用独立投递 Header。签名元信息命名与普通 API 请求和响应保持一致:
| Header | 说明 |
|---|---|
Content-Type | 固定为 application/json,表示 Webhook Body 是 UTF-8 JSON;不参与 Webhook 验签原文。 |
Request-Id | 本次投递请求追踪号,用于排障和日志关联。 |
Webhook-Secret-Id | 本次投递使用的 Webhook Secret 标识;用于在验签前选择本地保存的 Webhook Secret。商户在 Portal 生成或轮换 Webhook Secret 时获取并保存。 |
Timestamp | 本次投递签名时间,Unix 秒级时间戳文本。 |
Nonce | 本次投递随机串,重试时可能变化。 |
Signature | v1= 加小写 HMAC-SHA256 十六进制通知签名。 |
2. 选择 Webhook Secret
商户应根据 Webhook-Secret-Id 精确选择本地保存的 Webhook Secret。商户必须保存 Portal 展示的映射关系, 例如 whsecid_old_001 -> whsec_old、whsecid_new_002 -> whsec_new。找不到 Secret ID、Secret 已停用、或商户环境不匹配时, 必须拒绝处理该通知。
3. 检查时间戳和事件去重键
验签前先检查 Timestamp 是否超过允许偏差,默认建议 300 秒。重试投递会重新生成 Request-Id、Timestamp、Nonce 和 Signature,但 Body eventId 保持同一个事件 ID。商户必须按 Body eventId 做幂等。Header Timestamp 表示本次投递签名时间,并以文本发送;Body order.updateTime 表示本次通知订单状态的生效时间,以 JSON number 返回,同一事件重试时保持稳定。
4. 获取 Webhook 原始 Body
Webhook 验签必须使用收到的 UTF-8 原始 Body。不要先解析 JSON、格式化 JSON、替换换行或改变字段 顺序。很多框架读取 Body 后不能再次读取,接入时要在过滤器或中间件里提前缓存原始字节。
5. 构造 Webhook 验签原文
Webhook 验签原文固定三行,每一行后都必须追加换行字节 \n,包括最后一行:
投递时间戳文本\n
投递随机串\n
Webhook请求报文主体\n示例:
1780272000\n
nonce_fixture_webhook_payment_001\n
{"eventId":"evt_fixture_001","eventType":"PAYMENT_SUCCEEDED","merchantNo":"MCH_FIXTURE_001","order":{"platOrderNo":"PHI112606010000000001001","merchantOrderNo":"DEMO_PAY_202606010001","status":"SUCCEEDED","productCode":"PH_PHP_PAYMENT_QRPH_GCASH","country":"PH","amount":{"value":"100.50","currency":"PHP"},"referenceNo":"REF-20260601-0001","customData":{"cartId":"CART-10001","customerRef":"CUST-90001"},"createTime":1780272000,"updateTime":1780275600}}\n6. 计算并比对签名
使用 Webhook-Secret-Id 对应的 Webhook Secret,对验签原文计算 HMAC-SHA256,小写十六进制后加 v1= 前缀,并与请求头 Signature 做常量时间比较。
验签通过后再解析 JSON,并按 Body eventId 做幂等处理。验签失败、Webhook Secret ID 未知或 Body 被篡改时, 返回 4xx 或 5xx;不要更新订单状态。
7. 应答要求
商户只有在事件已可靠落库或进入可靠异步队列后,才返回任意 HTTP 2xx 响应,无需返回业务数据。 平台会将任意 2xx 响应视为接收成功;非 2xx 响应、超时或网络异常会触发重试。 业务处理耗时较长时,应先完成验签、幂等和落库,再异步处理业务。
