接口文档
环境信息
接口地址
生产环境
正式
https://api.huihanhc.com
所有接口正式生效并计费。
测试环境
测试
https://api-test.huihanhc.com
面向开发调试,不计费。
注意:测试环境与生产环境使用独立的商户配置,切勿将生产密钥用于测试环境。
接入流程
| 步骤 | 说明 | 预计耗时 |
|---|---|---|
| 1. 申请开通 | 联系我方商务对接,获取商户号(MerchantId)、AppKey、AppSecret | 1 个工作日 |
| 2. 开发对接 | 依据本文档完成签名算法实现和接口对接 | - |
鉴权机制
所有业务接口均需通过 HMAC-SHA256 签名认证,签名由客户端计算后置于 HTTP 请求头中。
请求头说明
| Header 名称 | 类型 | 必选 | 说明 | 示例 |
|---|---|---|---|---|
| X-Merchant-Id | string | *是 | 商户号,由平台分配 | - |
| X-App-Key | string | *是 | 应用公钥,由平台分配 | - |
| X-Timestamp | string | *是 | Unix 秒级时间戳,允许与网关时间 ±300 秒偏差 | 1755587400 |
| X-Nonce | string | *是 | 随机字符串,每个请求唯一,用于防止重放攻击 | a4588f2db8b8 |
| X-Signature | string | *是 | HMAC-SHA256 签名,Base64 编码,计算方式见下文 | U2FsdGVkX1... |
安全提示:AppSecret 是签名密钥,严禁暴露在客户端代码中,必须在服务端使用。
签名算法
1
计算 PathHash
PathHash = SHA256(HTTP方法 + 请求路径)
HTTP 方法使用全大写(如 POST、GET);请求路径不含域名
2
计算 BodyHash
BodyHash = SHA256(请求体原始内容)
请求体必须为原始 JSON 字符串,UTF-8 编码,不可修改空格、换行等字符
3
构造签名原串 SignStr
SignStr = MerchantId + Timestamp + Nonce + PathHash + BodyHash
按顺序直接拼接,无分隔符。SignStr 仅包含字母和数字
4
生成签名 Signature
Signature = Base64(HMAC-SHA256(AppSecret, SignStr))
HMAC-SHA256 的密钥为 AppSecret,消息为 SignStr,结果使用 Base64 编码
签名计算示例
以下为一组固定值的完整计算过程,可用于验证签名算法的正确性:
| 参数 | 值 |
|---|---|
| MerchantId | XXT20240001 |
| AppSecret | sk_xxt_live_7k9m2x4q6r8t0v3w5y7z9b1d3f5h8j |
| HTTP 方法 | POST |
| 请求路径 | /v1/id_verify |
| 请求体 | {"id_card":"123456789987654321","name":"测试"} |
| Timestamp | 1755587400 |
| Nonce | a4588f2db8b8 |
| PathHash (结果) | 3f08e4be594a9bd397473492c089a43905bfd06d57c656cc86fc360c457755ec |
| BodyHash (结果) | be22ced812fa1f598e708f819a4717607fd5d72c9b328d4be98c348d79be135a |
Node.js 签名示例
JavaScript / Node.js
// 签名算法示例(Node.js) const crypto = require('crypto'); const merchantId = 'XXT20240001'; const appSecret = 'sk_xxt_live_7k9m2x4q6r8t0v3w5y7z9b1d3f5h8j'; const method = 'POST'; const path = '/v1/id_verify'; const body = '{"id_card":"123456789987654321","name":"测试"}'; const timestamp = 1755587400; const nonce = 'a4588f2db8b8'; // 1. 计算 PathHash 和 BodyHash (SHA256 → Hex 小写) const pathHash = crypto.createHash('sha256') .update(method + path, 'utf8').digest('hex'); const bodyHash = crypto.createHash('sha256') .update(body, 'utf8').digest('hex'); // 2. 构造签名原串 (只含字母和数字) const signStr = merchantId + timestamp + nonce + pathHash + bodyHash; // 3. 生成签名 (HMAC-SHA256 → Base64) const signature = crypto.createHmac('sha256', appSecret) .update(signStr, 'utf8').digest('base64'); console.log({ signStr, signature });
API 接口
身份证二要素核验
请求参数
| 参数名 | 类型 | 必选 | 说明 | 示例 |
|---|---|---|---|---|
| id_card | string | *是 | 18 位身份证号码 | 123456789987654321 |
| name | string | *是 | 真实姓名 | 测试 |
请求示例(cURL)
curl -X POST https://api.example.com/v1/id_verify \ -H "Content-Type: application/json" \ -H "X-Merchant-Id: XXT20240001" \ -H "X-App-Key: pk_xxt_live_a3f8b2c1d9e0" \ -H "X-Timestamp: 1755587400" \ -H "X-Nonce: a4588f2db8b8" \ -H "X-Signature: U2FsdGVkX1..." \ -d '{"id_card":"123456789987654321","name":"测试"}'
响应参数
| 字段 | 类型 | 说明 |
|---|---|---|
| code | string | 业务状态码,成功为 "OK" |
| msg | string | 结果描述 |
| request_id | string | 请求唯一标识,排查问题时使用 |
| channel_id | string | 本次调用使用的上游通道 ID |
| cost_time | integer | 网关处理总耗时,单位毫秒 |
| data.verified | boolean | 是否完成核验 |
| data.match | boolean | 身份证号与姓名是否匹配 |
| data.message | string | 核验结果文字说明 |
响应示例(成功)
{
"code": "OK",
"msg": "成功",
"request_id": "abc123def456",
"channel_id": "-",
"cost_time": 256,
"data": {
"verified": true,
"match": true,
"message": "身份核验通过"
}
}
身份证三要素核验(人脸比对)
请求参数
| 参数名 | 类型 | 必选 | 说明 | 示例 |
|---|---|---|---|---|
| id_card | string | *是 | 18 位身份证号码 | 123456789987654321 |
| name | string | *是 | 真实姓名 | 测试 |
| photo | string | *是 | 人脸照片 Base64 编码(不含 data:image 前缀),详见下方图片要求 | /9j/4AAQSkZJRg... |
| bio | boolean | 否 | 是否启用活体检测。启用后耗时更长但安全性更高,默认 false | false |
图片要求:
格式:JPG / PNG;大小 ≤ 30KB(建议 10KB-30KB);推荐分辨率 480×640,最大不超过 1080P;
内容为正面清晰头像,无多人脸,无遮挡,光线自然,面部轮廓清晰,双眼间距建议不小于 90 像素。
响应参数
| 字段 | 类型 | 说明 |
|---|---|---|
| code | string | 业务状态码 |
| msg | string | 结果描述 |
| request_id | string | 请求唯一标识 |
| channel_id | string | 上游通道 ID |
| cost_time | integer | 总耗时,单位毫秒 |
| data.similarity | float | 相似度,范围 0-100 |
| data.ref_thresh | array | 参考阈值,依次为 [宽松 1e-3, 标准 1e-4, 严格 1e-5],一般取标准阈值判定 |
| data.pass | boolean | 是否通过,相似度 ≥ 标准阈值(83) 即为 true |
响应示例(比对通过)
{
"code": "OK",
"msg": "成功",
"request_id": "abc123def456",
"channel_id": "-",
"cost_time": 756,
"data": {
"similarity": 95.23,
"ref_thresh": [75, 83, 88],
"pass": true
}
}
响应示例(相似度不足)
{
"code": "UPSTREAM_FAILED",
"msg": "人脸比对相似度低于阈值",
"request_id": "abc123",
"channel_id": "-",
"cost_time": 756,
"data": {
"similarity": 45.12,
"ref_thresh": [75, 83, 88],
"pass": false
}
}
错误码说明
网关错误码
| HTTP 状态 | 业务码 | 说明 | 处理建议 |
|---|---|---|---|
| 401 | 401 | 缺少签名认证头 | 检查是否携带了全部 5 个鉴权头字段 |
| 401 | 401 | 签名无效 | 核查签名算法是否正确,AppSecret 是否匹配,Body 与签名时是否一致 |
| 401 | 401 | 时间戳过期 | 检查服务器时钟是否同步,网关允许 ±300 秒偏差 |
| 401 | 401 | Nonce 已使用 | 确保每次请求使用唯一随机值,Nonce 在 10 分钟内不可重复 |
| 401 | 401 | 商户已禁用 | 联系我方商务确认商户状态 |
| 404 | 404 | 接口不存在 | 检查请求路径是否正确,注意是否与文档一致 |
| 429 | 429 | 请求过于频繁 | 请控制请求频率在商户额度范围内,可申请提升 QPS |
业务错误码
| 业务码 | 说明 | 处理建议 |
|---|---|---|
| 20000 | 成功 | — |
| 204002 | 身份信息不存在 | 检查身份证号和姓名是否真实有效 |
| 204003 | 身份信息格式不正确 | 身份证号需通过校验位算法,姓名不可为空或超长 |
| 204004 | 人脸图片质量不达标 | 更换符合要求的高清正面照 |
| 204005 | 活体检测失败 | 请正对镜头按要求完成活体动作 |
| 204006 | 人脸与身份证照不匹配 | 请使用本人照片 |
完整代码示例
C# (.NET) 完整示例
C# / .NET 8
using System.Security.Cryptography; using System.Text; using System.Text.Json; // 配置 var merchantId = "XXT20240001"; var appKey = "pk_xxt_live_a3f8b2c1d9e0"; var appSecret = "sk_xxt_live_7k9m2x4q6r8t0v3w5y7z9b1d3f5h8j"; var baseUrl = "https://api.example.com"; // 请求体 var payload = new { id_card = "123456789987654321", name = "测试" }; var body = JsonSerializer.Serialize(payload); var method = "POST"; var path = "/v1/id_verify"; // 签名参数 var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(8)).ToLowerInvariant(); var pathHash = ComputeHash(method + path); var bodyHash = ComputeHash(body); var signStr = $"{merchantId}{timestamp}{nonce}{pathHash}{bodyHash}"; var signature = ComputeSignature(appSecret, signStr); // 发送请求 using var http = new HttpClient(); http.DefaultRequestHeaders.Add("X-Merchant-Id", merchantId); http.DefaultRequestHeaders.Add("X-App-Key", appKey); http.DefaultRequestHeaders.Add("X-Timestamp", timestamp); http.DefaultRequestHeaders.Add("X-Nonce", nonce); http.DefaultRequestHeaders.Add("X-Signature", signature); var res = await http.PostAsync(baseUrl + path, new StringContent(body, Encoding.UTF8, "application/json")); var resBody = await res.Content.ReadAsStringAsync(); // —— 工具函数 —— static string ComputeHash(string input) { using var sha = SHA256.Create(); return Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(input))) .ToLowerInvariant(); } static string ComputeSignature(string key, string data) { using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)); return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(data))); }
Java 完整示例
Java / JDK 11+
import java.net.URI; import java.net.http.*; import java.nio.charset.StandardCharsets; import java.security.*; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public class GatewaySignDemo { static final String MERCHANT_ID = "XXT20240001"; static final String APP_KEY = "pk_xxt_live_a3f8b2c1d9e0"; static final String APP_SECRET = "sk_xxt_live_7k9m2x4q6r8t0v3w5y7z9b1d3f5h8j"; static final String BASE_URL = "https://api.example.com"; static final MessageDigest SHA; static { try { SHA = MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } public static String hex(byte[] b) { StringBuilder sb = new StringBuilder(); for (byte x : b) sb.append(String.format("%02x", x)); return sb.toString(); } public static String sign(String key, String data) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key.getBytes(), "HmacSHA256")); return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes())); } public static void main(String[] args) throws Exception { String method = "POST"; String path = "/v1/id_verify"; String body = "{\"id_card\":\"123456789987654321\",\"name\":\"测试\"}"; String timestamp = String.valueOf(System.currentTimeMillis() / 1000); SecureRandom r = new SecureRandom(); String nonce = String.format("%016x", new java.math.BigInteger(64, r)); String pathHash = hex(SHA.digest((method + path).getBytes(StandardCharsets.UTF_8))); String bodyHash = hex(SHA.digest(body.getBytes(StandardCharsets.UTF_8))); String signStr = MERCHANT_ID + timestamp + nonce + pathHash + bodyHash; String signature = sign(APP_SECRET, signStr); HttpClient client = HttpClient.newHttpClient(); HttpRequest req = HttpRequest.newBuilder() .uri(URI.create(BASE_URL + path)) .header("Content-Type", "application/json") .header("X-Merchant-Id", MERCHANT_ID) .header("X-App-Key", APP_KEY) .header("X-Timestamp", timestamp) .header("X-Nonce", nonce) .header("X-Signature", signature) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponseresp = client.send(req, HttpResponse.BodyHandlers.ofString()); System.out.println(resp.body()); } }