Разработчикам/Начало работы
Аутентификация
В каждом запросе к API передаются ID вашего ключа и подпись. Подпись доказывает, что запрос отправили вы, и никто не сможет изменить или повторить его. Сам секрет подписи никогда не передаётся.
См. также: Каталог и заказы · Безопасность · README
#Учётные данные
| Что | Пример | Секрет? |
|---|---|---|
| ID ключа | ck_live_... | Нет. Он лишь говорит, какой ключ вы используете. |
| Секрет подписи | cs_live_... | Да. Им вы подписываете запросы. Он никогда не покидает ваш сервер. |
| Merchant ID | M00000001 | Нет. В запросах к API он необязателен. |
- Ключи создаёт Owner в личном кабинете (см. API-ключи).
- Кабинет показывает секрет подписи один раз — при создании ключа. Больше его не покажет никто, даже CardV.
- Ключи Live начинаются с
ck_live_иcs_live_. Ключи Sandbox начинаются сck_test_иcs_test_. Ключ из одной среды никогда не работает в другой. - API начинает работать только после того, как CardV одобрит вашу компанию.
После этого
GET/accountпоказывает"api_access_enabled": true.
#Заголовки
В каждом запросе, включая GET-запросы, передавайте четыре заголовка:
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89X-Key-Id— публичный ID вашего ключа.X-Timestamp— текущее время Unix в секундах. Оно должно расходиться с часами CardV не больше чем на 5 минут (300 секунд).X-Nonce— случайное значение, которое вы никогда не используете повторно. Возьмите 32 случайных шестнадцатеричных символа.X-Signature— подпись, о ней рассказано в разделе Подпись запроса. Только строчные шестнадцатеричные символы.
Можно также передать X-Merchant-Id. Если вы его передаёте, он должен совпадать с ключом.
#Подпись запроса
Превратите тело запроса в байты один раз. Именно эти байты вы подпишете и отправите. У
GET-запроса тело пустое.Посчитайте хеш тела:
BODY_HASH= SHA-256 от тела в виде строчной шестнадцатеричной строки. Для пустого тела этоe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.Соедините эти пять строк через перевод строки (
\n), без перевода строки в конце:<METHOD> <PATH with query string> <X-Timestamp> <X-Nonce> <BODY_HASH>METHODпишется заглавными буквами, напримерGETилиPOST. Путь включает строку запроса точно в том виде, в каком вы её отправляете, например/api/v1/skus?limit=50.Подпишите результат:
X-Signature= HMAC-SHA256 от этого текста, ключ — ваш секрет подписи. Запишите результат строчными шестнадцатеричными символами.
Запросы пополнения мобильной связи подписываются так же, но со своим путём, например /api/v1/recharge/orders.
Самая частая ошибка — подписать одну версию JSON, а отправить другую.
Например, {"a":1} и {"a": 1} — это разные байты.
Убедитесь, что HTTP-библиотека не меняет тело и не переставляет параметры строки запроса после подписи.
Тестовые примеры. Проверьте свой код на этих значениях. Секрет ненастоящий.
Signing secret cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdefЗапрос GET:
Method GET
Path /api/v1/skus?limit=50
BODY_HASH e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature 58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5Запрос POST к /api/v1/orders с таким телом (106 байт, одна строка, без перевода строки в конце):
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}BODY_HASH 5814ffcdadfa9eb69348150f9fd6895c00b4c37cf6cd0202b83174354645023e
X-Signature e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89#Примеры кода
Все примеры дают результаты из тестовых примеров. Загружайте секрет из хранилища секретов, а не из кода.
cURL (bash + OpenSSL)
BASE_URL="https://sandbox.cardv.net"
REQ_PATH="/api/v1/orders" # do not call this variable PATH
BODY='{"external_order_id":"SHOP-10001","items":'
BODY+='[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}'
TS=$(date +%s)
NONCE=$(openssl rand -hex 16)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.*= //')
SIG=$(printf 'POST\n%s\n%s\n%s\n%s' "$REQ_PATH" "$TS" "$NONCE" "$BODY_HASH" \
| openssl dgst -sha256 -hmac "$CARDV_SIGNING_SECRET" -hex | sed 's/^.*= //')
curl -sS -X POST "$BASE_URL$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Key-Id: $CARDV_KEY_ID" \
-H "X-Timestamp: $TS" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIG" \
--data-raw "$BODY"Используйте --data-raw, а не -d @file. -d удаляет переводы строк, и отправленные байты не совпадут с подписанными.
Для GET-запроса подпишите GET, путь со строкой запроса и хеш пустого тела, а тело не отправляйте.
Python (requests)
import hashlib, hmac, json, os, secrets, time
import requests
BASE_URL = "https://sandbox.cardv.net"
KEY_ID = os.environ["CARDV_KEY_ID"]
SIGNING_SECRET = os.environ["CARDV_SIGNING_SECRET"]
def signed_headers(method: str, path: str, body: bytes) -> dict:
ts, nonce = str(int(time.time())), secrets.token_hex(16)
body_hash = hashlib.sha256(body).hexdigest()
text = "\n".join([method, path, ts, nonce, body_hash])
signature = hmac.new(SIGNING_SECRET.encode(), text.encode(), hashlib.sha256).hexdigest()
return {"X-Key-Id": KEY_ID, "X-Timestamp": ts, "X-Nonce": nonce, "X-Signature": signature}
def cardv_get(path: str) -> requests.Response:
# Put the query string in `path` so the signed path and the sent path are identical.
return requests.get(BASE_URL + path, headers=signed_headers("GET", path, b""), timeout=30)
def cardv_post(path: str, payload: dict) -> requests.Response:
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
headers = {**signed_headers("POST", path, body), "Content-Type": "application/json"}
return requests.post(BASE_URL + path, data=body, headers=headers, timeout=30)
resp = cardv_post("/api/v1/orders", {
"external_order_id": "SHOP-10001",
"items": [{"sku_id": "S000001", "quantity": 1, "expected_unit_price": "9.25"}],
})
print(resp.status_code, resp.json())
order_id = resp.json()["order"]["order_id"]
print(cardv_get(f"/api/v1/orders/{order_id}").json()["status"])Node.js 18+ (встроенный fetch)
import crypto from "node:crypto";
const BASE_URL = "https://sandbox.cardv.net";
const { CARDV_KEY_ID, CARDV_SIGNING_SECRET } = process.env;
function signedHeaders(method, path, body) {
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const text = [method, path, timestamp, nonce, bodyHash].join("\n");
const signature = crypto.createHmac("sha256", CARDV_SIGNING_SECRET).update(text, "utf8").digest("hex");
return { "X-Key-Id": CARDV_KEY_ID, "X-Timestamp": timestamp, "X-Nonce": nonce, "X-Signature": signature };
}
async function cardvGet(path) {
const res = await fetch(BASE_URL + path, { headers: signedHeaders("GET", path, Buffer.alloc(0)) });
return { status: res.status, body: await res.json() };
}
async function cardvPost(path, payload) {
const body = Buffer.from(JSON.stringify(payload), "utf8"); // serialize once
const headers = { ...signedHeaders("POST", path, body), "Content-Type": "application/json" };
const res = await fetch(BASE_URL + path, { method: "POST", headers, body });
return { status: res.status, body: await res.json() };
}
console.log(await cardvPost("/api/v1/orders", {
external_order_id: "SHOP-10001",
items: [{ sku_id: "S000001", quantity: 1, expected_unit_price: "9.25" }],
}));
console.log(await cardvGet("/api/v1/balance"));PHP (только подпись)
<?php
function cardv_sign(
string $secret, string $method, string $path, string $body, string $ts, string $nonce
): string {
$text = implode("\n", [$method, $path, $ts, $nonce, hash('sha256', $body)]);
return hash_hmac('sha256', $text, $secret); // lowercase hex
}
// Send exactly $body. For a GET call use $body = ''.
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = cardv_sign($signingSecret, 'POST', '/api/v1/orders', $body, $ts, $nonce);
// Headers: X-Key-Id, X-Timestamp, X-Nonce, X-Signature#Ошибки
Все эти ошибки возвращают HTTP 403 (не 401). Исключение — превышение лимита запросов: для него приходит 429. Тело ответа выглядит так:
{"detail": "Invalid HMAC signature."}| Проблема | Что делать |
|---|---|
| Неизвестный или отключённый ID ключа | Проверьте X-Key-Id и среду (Live или Sandbox). |
| Доступ к API не включён | Дождитесь, пока CardV одобрит вашу компанию. |
| IP вашего сервера не разрешён | Добавьте его в список разрешённых IP в кабинете. |
| Метод доступен только в кабинете | Воспользуйтесь кабинетом. С ключом работают только шестнадцать методов. |
| Подписи нет или она неверна | Исправьте код подписи. Проверьте его на тестовых примерах. |
X-Timestamp слишком старый или опережает часы CardV | Синхронизируйте часы сервера (NTP). |
| Nonce уже использован | Для каждого запроса берите новый случайный nonce. |
| Слишком много запросов (429) | Подождите столько секунд, сколько указано в Retry-After, и повторите. |
Важно:
- При повторной отправке запроса создайте новые время, nonce и подпись. Тело оставьте прежним. См. безопасный повтор.
- Nonce считается использованным, даже если подпись была неверной.
- CardV записывает неудачные попытки и оповещает свою команду, если их много.
#Список разрешённых IP
Список разрешённых IP позволяет пользоваться вашим ключом только вашим серверам. Им управляет Owner в личном кабинете.
- Список необязателен. Пока в нём нет правил, запросы принимаются с любого IP.
- Как только появится хотя бы одно правило, запросы с других IP получат HTTP 403.
- Правила выглядят так:
203.0.113.10/32(IPv4) или2001:db8::/48(IPv6). - Список действует только на запросы с API-ключом, а не на вход в кабинет.
- Прежде чем включать список, добавьте IP всех своих серверов, включая NAT-шлюзы и резервные регионы.
#API-ключи
API-ключ даёт доступ ровно к шестнадцати методам из раздела Кратко об API. Среди них — оформление заказов и получение кодов, поэтому берегите секрет подписи как пароль Owner.
Управлять ключами может только Owner — в кабинете, в разделе Integrations → API keys. В Live и Sandbox это делается отдельно.
- Создайте ключ и дайте ему название. Кабинет покажет ID ключа и секрет подписи. Сразу скопируйте секрет в хранилище секретов. Он показывается только этот один раз.
- Перевыпустите секрет, если вы его потеряли. ID ключа при этом не меняется. Старый секрет сразу перестаёт работать, поэтому немедленно установите новый на все свои серверы.
- Отключите ключ, когда он больше не нужен. Это происходит сразу и навсегда. После этого отключённый ключ можно удалить из списка. CardV сохраняет его историю.
Как сменить ключ без простоя:
- Создайте новый ключ и сохраните его секрет.
- Установите его на все свои серверы.
- Убедитесь в кабинете, что старый ключ больше не используется.
- Отключите старый ключ.
Меняйте ключи хотя бы раз в год, а также каждый раз, когда уходит сотрудник, у которого был доступ. Если ключ мог утечь, сначала отключите его, а потом разбирайтесь.
#Старые ключи (legacy)
Ключи, созданные до октября 2026 года, начинаются с cvb2b_ и используют старые заголовки
X-Merchant-Id и X-Api-Key, а подписываются с ними только запросы POST.
Старый ключ передаётся в каждом запросе, поэтому он легче утекает.
Live и Sandbox больше не принимают такие ключи. Перейдите на новый ключ уже сейчас: создайте его в кабинете, переведите код на заголовки выше, а затем отключите старый ключ.
Вопросы по интеграции? Напишите на [email protected], указав ваш Merchant ID и ID заказа или запроса.