---
title: CP-HMAC 请求签名
description: 如何为 Coinetic 加密支付 API 请求签名：CP-HMAC Authorization 请求头、签名字符串、nonce、允许的时钟偏差以及用于自测的标准测试向量。
url: https://coinetic.money/zh/docs/authentication/
lang: zh-Hans
---

# CP-HMAC 请求签名

每个发往 `/v1/*` 的请求都使用 API 密钥的 secret 签名，secret 本身不会在网络上传输。官方客户端会自动完成签名 —— 本节面向自行实现客户端的开发者。

## 请求头

```http
Authorization: CP-HMAC key=<key_id>, ts=<unix_seconds>, nonce=<nonce>, sig=<hex>
```

| 字段 | 格式 |
| --- | --- |
| `key` | 密钥 id：`ak_` + 16–40 个 `[a-z0-9]` 字符 |
| `ts` | 当前时间，Unix 秒。与服务器时钟的允许偏差为 **±5 分钟** |
| `nonce` | 一次性字符串 `[A-Za-z0-9_-]{16,64}`；重复使用同一 nonce 会被拒绝 |
| `sig` | 64 个小写十六进制字符 |

## 签名内容

```
string = METHOD + "\n" + PATH_WITH_QUERY + "\n" + ts + "\n" + nonce + "\n" + hex(sha256(body))
sig    = hex(HMAC-SHA256(secret, string))
```

- `METHOD` —— `GET`、`POST` 等。
- `PATH_WITH_QUERY` —— 路径和查询字符串，必须与实际发送的完全一致：`/v1/invoices?status=paid&limit=50`。协议和主机名不参与签名。
- 请求体按字节逐一签名。请对将要发送的那段 JSON 字符串签名，而不是重新序列化后的对象。
- 无请求体时，`hex(sha256(""))` = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`。

**Node.js** — `sign.js`

```js
import { createHash, createHmac, randomBytes } from "node:crypto";

export function sign(keyId, secret, method, pathWithQuery, body = "") {
  const ts = Math.floor(Date.now() / 1000).toString();
  const nonce = randomBytes(16).toString("hex");
  const bodyHash = createHash("sha256").update(body).digest("hex");
  const sig = createHmac("sha256", secret)
    .update(`${method}\n${pathWithQuery}\n${ts}\n${nonce}\n${bodyHash}`)
    .digest("hex");
  return `CP-HMAC key=${keyId}, ts=${ts}, nonce=${nonce}, sig=${sig}`;
}
```

**PHP** — `sign.php`

```php
function cp_sign(string $keyId, string $secret, string $method, string $pathWithQuery, string $body = ''): string {
    $ts = (string) time();
    $nonce = bin2hex(random_bytes(16));
    $str = $method . "\n" . $pathWithQuery . "\n" . $ts . "\n" . $nonce . "\n" . hash('sha256', $body);
    $sig = hash_hmac('sha256', $str, $secret);
    return "CP-HMAC key=$keyId, ts=$ts, nonce=$nonce, sig=$sig";
}
```

**Python** — `sign.py`

```py
import hashlib, hmac, secrets, time

def cp_sign(key_id: str, secret: str, method: str, path_with_query: str, body: bytes = b"") -> str:
    ts = str(int(time.time()))
    nonce = secrets.token_hex(16)
    msg = f"{method}\n{path_with_query}\n{ts}\n{nonce}\n{hashlib.sha256(body).hexdigest()}"
    sig = hmac.new(secret.encode(), msg.encode(), hashlib.sha256).hexdigest()
    return f"CP-HMAC key={key_id}, ts={ts}, nonce={nonce}, sig={sig}"
```

**Go** — `sign.go`

```go
sig := cryptopayclient.SignRequest(secret, "POST", "/v1/invoices", ts, nonce, body)
req.Header.Set("Authorization", fmt.Sprintf("CP-HMAC key=%s, ts=%d, nonce=%s, sig=%s", keyID, ts, nonce, sig))
```

## 测试向量

用以下数值校验您的实现 —— 四个官方客户端均使用同一组数值进行测试。

| Secret | `test-secret-0123456789abcdef0123456789` |
| --- | --- |
| 请求 1 | `POST /v1/invoices`，ts `1760000000`，nonce `nonce-0000000001`，请求体 `{"order_id":"WISH-1"}` |
| sig 1 | `2c769f6a5be7b80871e03851ccfe84002130ec48a45348bd821752a6d45960ec` |
| 请求 2 | `GET /v1/invoices/inv_abcdefabcdefabcdef`，ts `1760000000`，nonce `nonce-0000000002`，无请求体 |
| sig 2 | `a12924acf8a772c6f3fadb8874ff69e9934046ccd2fa5563b23fec3e98e1665b` |

## 如果收到 401

任何签名错误都会返回 `401 unauthorized`，且错误文本完全相同：服务器不会提示具体哪里出错。常见原因：

- 服务器时钟偏差超过 5 分钟 —— 请启用 NTP；
- 签名的路径不一致 —— 缺少 query 或参数顺序不同；
- 签名后请求体被重新序列化；
- nonce 被重复使用；
- 密钥已被吊销。

`403 merchant_suspended` —— 签名正确，但商户已被暂停。

## 密钥管理

- 密钥在后台的「密钥与 Webhook」中创建。secret 只显示**一次**。
- 最多可同时启用 10 个密钥。轮换方法：创建新密钥，切换服务器配置，吊销旧密钥。吊销立即生效。
- 请将 secret 保存在密钥管理服务或服务器环境变量中，不要写进代码或 git。
- 频率限制约为每个商户每秒 20 个请求，并留有突发余量；超出时返回 `429 rate_limited`。
