入门

English

签名与验签

所有请求使用 HMAC-SHA256 签名,HansaPay 的响应与通知也按同样规则验签。

认证方式

所有商户主动调用 HansaPay 系统的接口都必须携带以下请求头:

  • X-MerchantID
  • X-Timestamp
  • X-Nonce
  • X-Sign

签名算法:

  • HMAC-SHA256
  • 输出格式:十六进制小写字符串

字符编码与请求格式

  • 请求方法:POST
  • Content-Type:application/json
  • 请求体必须参与签名
  • 签名时使用原始 JSON 字节串
  • 不要对 JSON 做二次美化、trim、字段重排后再验签

请求签名规则

待签名字符串

HansaPay 系统按以下格式验签:

Text
merchantID + "\n" + timestamp + "\n" + nonce + "\n" + rawBody

例如:

Text
M123456
1719200000
8f31a2bc9d4e6f70
{"payment_method":"CARD","merchant_order_no":"M202406240001","trans_amount":{"currency":"USD","value":"99.99"},"notify_url":"https://merchant.example.com/callback/payment","trade_info":{"goods_name":"VIP","description":"Monthly subscription"},"card":{"card_number":"4111111111111111","cardholder_name":"JOHN DOE","exp_month":12,"exp_year":2028,"cvc":"123"},"billing_address":{"country":"US","first_name":"John","last_name":"Doe","email":"[email protected]","phone":"15551234567","state":"CA","city":"San Francisco","address":"Market Street","street_number":"1355","postal_code":"94103"},"device_ip":"203.0.113.10","metadata":"biz=member"}

签名计算

伪代码:

Text
sign = hex_lower( HMAC_SHA256(secret_key, sign_payload) )

其中:

  • merchantID:HansaPay 系统分配的商户号
  • secret_key:HansaPay 系统分配的商户 API 密钥
  • timestamp:Unix 秒级时间戳
  • nonce:随机字符串,建议 16 到 32 位
  • rawBody:HTTP 原始请求体字符串

Java 示例

Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public class SignDemo {
    public static String hmacSha256Hex(String data, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
        mac.init(keySpec);
        byte[] raw = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        StringBuilder sb = new StringBuilder();
        for (byte b : raw) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public static void main(String[] args) throws Exception {
        String merchantId = "M123456";
        String timestamp = "1719200000";
        String nonce = "8f31a2bc9d4e6f70";
        String rawBody = "{\"order_no\":\"O202406240001\"}";
        String secret = "your_merchant_secret";

        String payload = merchantId + "\n" + timestamp + "\n" + nonce + "\n" + rawBody;
        String sign = hmacSha256Hex(payload, secret);
        System.out.println(sign);
    }
}

PHP 示例

PHP
<?php

$merchantId = 'M123456';
$timestamp = '1719200000';
$nonce = '8f31a2bc9d4e6f70';
$rawBody = '{"order_no":"O202406240001"}';
$secret = 'your_merchant_secret';

$payload = $merchantId . "\n" . $timestamp . "\n" . $nonce . "\n" . $rawBody;
$sign = hash_hmac('sha256', $payload, $secret);

echo $sign . PHP_EOL;

Go 示例

Go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
)

func main() {
	merchantID := "M123456"
	timestamp := "1719200000"
	nonce := "8f31a2bc9d4e6f70"
	rawBody := `{"order_no":"O202406240001"}`
	secret := "your_merchant_secret"

	payload := merchantID + "\n" + timestamp + "\n" + nonce + "\n" + rawBody
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(payload))
	sign := hex.EncodeToString(mac.Sum(nil))

	fmt.Println(sign)
}

请求头示例

HTTP
POST /api/v1/payments/query HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-MerchantID: M123456
X-Timestamp: 1719200000
X-Nonce: 8f31a2bc9d4e6f70
X-Sign: 7f7d6e3d2baf9b9bd0e4d8d9a5d2c8d9f7c4e1a2b3c4d5e6f708091011121314

HansaPay 系统响应验签

商户也应对 HansaPay 系统的响应验签。响应头使用同样的键:

  • X-MerchantID
  • X-Timestamp
  • X-Nonce
  • X-Sign

验签规则:

Text
merchantID + "\n" + timestamp + "\n" + nonce + "\n" + rawResponseBody

说明:

  • 响应 body 也必须按原始字符串参与验签
  • 不要先反序列化再重新序列化后验签
  • 如验签失败,应视为响应不可信
  • 401 与 403 鉴权错误不带签名

签名失败排查清单

当 HansaPay 系统返回以下错误时,优先检查签名链路:

  • 401,Missing authentication headers 或 Merchant ID not found or invalid
  • 403,Invalid signature

建议按下面顺序排查:

商户号是否正确

  • X-MerchantID 必须使用 HansaPay 系统分配的商户号
  • 不要传商户名称、登录账号或内部用户 ID

密钥是否正确

  • 确认使用的是商户 API 密钥,不是登录密码
  • 确认没有多环境混用
  • 测试环境和生产环境密钥通常不同

待签名字符串是否完全一致

HansaPay 系统验签格式固定为:

Text
merchantID + "\n" + timestamp + "\n" + nonce + "\n" + rawBody

常见错误:

  • 少了换行符
  • 使用了 \r\n 而不是 \n
  • header 顺序拼错
  • body 前后被 trim
  • body 不是原始字符串,而是对象重新序列化后的字符串

JSON 字段名是否写错

商户接口使用的是 snake_case,例如:

  • payment_method
  • merchant_order_no
  • notify_url
  • trade_info.goods_name

不要混用旧字段名:

  • paymentMethod
  • merchantOrderNo
  • goodsName

JSON 是否被二次格式化

签名必须基于实际发出的原始 body。

例如这两段 JSON 业务上等价,但签名结果不同:

JSON
{"order_no":"O1","status":"SUCCESS"}
JSON
{
  "order_no": "O1",
  "status": "SUCCESS"
}

因此:

  • 先生成最终请求 body
  • 再用这段最终 body 去签名
  • 不要签名之后再改空格、缩进或字段顺序

时间戳是否异常

  • X-Timestamp 必须使用秒级 Unix 时间戳
  • 不要传毫秒
  • 不要传格式化时间字符串

正确示例:

Text
1719200000

错误示例:

Text
1719200000123
2026-06-24T10:00:00+08:00

nonce 是否为空

  • X-Nonce 不能为空
  • 建议 16 到 32 位随机字符串

HMAC 输出格式是否正确

HansaPay 系统要求:

  • 算法:HMAC-SHA256
  • 输出:十六进制小写字符串

不要使用:

  • Base64
  • 十六进制大写
  • MD5
  • RSA

Content-Type 是否正确

请使用:

Text
Content-Type: application/json

验签失败时建议记录的日志

商户本地建议打印:

  • merchant_id
  • timestamp
  • nonce
  • raw_body
  • sign_payload
  • received_sign
  • expected_sign

这样可以最快定位是 header 问题、body 格式问题还是密钥问题。

金额限制错误排查

如果 HansaPay 系统返回:

  • 2006:代收金额不满足通道限制
  • 5008:代付金额不满足通道限制

请检查:

  1. trans_amount.value 是否低于当前可用通道最小限额
  2. trans_amount.value 是否高于当前可用通道最大限额
  3. 当前支付方式下,商户已启用通道是否都不接受该金额
  4. 这类错误与签名无关,通常不需要排查 X-Sign