Skip to content

签名与商户 API 规范

本文说明 SoPay 商户对接 API 的通用约定、RSA2 签名与异步回调验签,适用于全部国家/地区接入。

当前生产网关基于 REST JSON + Header 签名,不再使用旧版 appId / bizContent 信封结构。

基础约定

项目说明
API 前缀/api/v1/merchant
字符编码UTF-8
金额字符串或 JSON number,最多 2 位小数,必须大于 0
国家/币种countryCodecurrency 须与商户开户国家配置一致(如泰国 TH + THB

通用响应结构

json
{
  "code": 0,
  "msg": "ok",
  "data": {}
}
字段类型说明
codenumber0 表示成功
msgstring提示信息
dataobject业务数据

失败时 code0msg 为可读错误说明(见各国附录 错误码)。

鉴权请求头(必填)

所有 /api/v1/merchant/* 接口均需在 HTTP Header 携带:

Header类型说明
X-Merchant-Nostring商户号,开户后分配,如 M42
X-TimestampstringUnix 时间戳,
X-Noncestring随机唯一串,防重放
X-SignstringRSA2 签名,Base64 编码

POST 请求另需:Content-Type: application/json

安全策略(开户默认)

配置项默认值说明
enableIpWhitelistfalsetrue 时仅允许白名单 IP 调用
enableNonceChecktrue同一 nonce 在 TTL 内不可重复使用
requestExpireSeconds300请求时间戳允许偏差(秒)

RSA2 签名算法

  • 算法:RSA PKCS#1 v1.5 + SHA-256(又称 RSA2)
  • 密钥:2048 位及以上 RSA 密钥对
  • 商户侧:保管 私钥,用于请求签名;向平台提交 公钥 PEM
  • 平台侧:回调时使用 平台私钥 签名;商户使用 平台公钥 验签

待签名字符串

  1. 收集参与签名的参数:
    • GET:URL Query 全部参数
    • POST:JSON Body 顶层字段(FlattenJSONMap:嵌套 object/array 会 JSON 序列化为字符串值)
  2. 将 Header 中的 timestampnonce 加入参数 Map(键名小写:timestampnonce
  3. 排除:键名为 sign(不区分大小写)、值为空的字段
  4. 按参数名 ASCII 升序 排序
  5. 拼接:key1=value1&key2=value2&...

示例(创建代付):

amount=1000.00&countryCode=TH&currency=THB&merchantOrderNo=ORD20260608001&nonce=8f3a2c1b&receiverAccount=1234567890&receiverName=John&timestamp=1718198400

生成签名

signature = Base64( RSA-SHA256-Sign(merchant_private_key, UTF8(sign_content)) )

将结果放入 Header X-Sign

验签(商户收到平台回调时)

  1. 读取回调 Body JSON 与 Header X-TimestampX-NonceX-Sign
  2. 将 Body 顶层 字段扁平化为 map[string]string,加入 timestampnonce
  3. 按 key 字典序拼接 k=v&... 得到 sign_content
  4. 使用 平台公钥 验证 X-Sign验签,不是解密;Body 为明文 JSON)

数字字段: 回调中 completedAt 等为 JSON number(Unix 秒)。扁平化时为十进制整数字符串(如 1782121882)。金额字段为 string。详见 异步回调接入指南 · Body 扁平化

重要

处理异步回调时 必须验签,验签通过且业务处理成功后再更新订单状态。

完整流程(幂等、应答、重试、代码示例)见 异步回调接入指南

异步回调 ACK

平台 POST 到您配置的 notifyUrl(或商户默认 callbackUrl)后,仅在以下条件视为成功:

  • HTTP 状态码 200
  • 响应 Body 去首尾空白后 严格等于 OK(建议 Content-Type: text/plain

否则会按退避策略重试(最多约 200 次)。详见 异步回调接入指南

代码示例(Go)

go
package main

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"sort"
	"strings"
)

func buildSignContent(params map[string]string) string {
	keys := make([]string, 0, len(params))
	for k, v := range params {
		if strings.EqualFold(k, "sign") || strings.TrimSpace(v) == "" {
			continue
		}
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, k+"="+params[k])
	}
	return strings.Join(parts, "&")
}

func signRSA2(privateKeyPEM, content string) (string, error) {
	block, _ := pem.Decode([]byte(privateKeyPEM))
	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		key2, err2 := x509.ParsePKCS1PrivateKey(block.Bytes)
		if err2 != nil {
			return "", err
		}
		key = key2
	}
	rsaKey := key.(*rsa.PrivateKey)
	sum := sha256.Sum256([]byte(content))
	sig, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, sum[:])
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(sig), nil
}

func main() {
	params := map[string]string{
		"merchantOrderNo": "ORD001",
		"countryCode":     "TH",
		"currency":        "THB",
		"amount":          "1000.00",
		"receiverName":    "John",
		"receiverAccount": "1234567890",
		"timestamp":       "1718198400",
		"nonce":           "uuid-or-random",
	}
	content := buildSignContent(params)
	sig, _ := signRSA2("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----", content)
	fmt.Println(sig)
}

商户接入清单

  1. 向运营提交商户 RSA 公钥 PEM(2048 位)
  2. 获取 merchantNo平台公钥(用于回调验签)
  3. 配置代付默认 callbackUrl、手续费规则(运营后台)
  4. 实现四类接口:代收、代付、查询、钱包(见各国文档)
  5. 代付前确保商户钱包余额 ≥ 冻结金额(netAmount = amount + fee
  6. 回调接口返回 OK

基于 MIT 许可证发布。

2-1-2 Nihonbashi-Hongokucho,Chuo-ku,Tokyo