Документация / Интеграция
Подпись запросов CP-HMAC
Каждый запрос к /v1/* подписывается секретом API-ключа. Сам секрет по сети не передаётся. Готовые клиенты делают это за вас — раздел для тех, кто пишет свой.
Заголовок
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 hex-символа в нижнем регистре |
Что подписывается
text
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— путь и query-строка точно так, как они уйдут в запросе:/v1/invoices?status=paid&limit=50. Схема и хост не подписываются.- Тело подписывается байт в байт. Подпишите ту же строку JSON, которую отправите, а не пересериализованный объект.
- Для запросов без тела
hex(sha256(""))=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.
sign.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}`;
}sign.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";
}sign.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}"sign.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))Эталонные значения
Сверьте свою реализацию — на этих же значениях проверяются все четыре официальных клиента.
| Секрет | 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 — подпись верна, но мерчант приостановлен.
Ключи
- Ключи создаются в кабинете, раздел «Ключи и вебхуки». Секрет показывается один раз.
- Одновременно действует до 10 ключей. Ротация: создайте новый, переключите сервер, отзовите старый. Отзыв действует сразу.
- Храните секрет в секрет-менеджере или переменной окружения сервера, не в коде и не в git.
- Лимит — около 20 запросов в секунду на мерчанта с запасом на всплески; при превышении —
429 rate_limited.