Разработчикам/Начало работы

Аутентификация

В каждом запросе к API передаются ID вашего ключа и подпись. Подпись доказывает, что запрос отправили вы, и никто не сможет изменить или повторить его. Сам секрет подписи никогда не передаётся.

См. также: Каталог и заказы · Безопасность · README

#Учётные данные

ЧтоПримерСекрет?
ID ключаck_live_...Нет. Он лишь говорит, какой ключ вы используете.
Секрет подписиcs_live_...Да. Им вы подписываете запросы. Он никогда не покидает ваш сервер.
Merchant IDM00000001Нет. В запросах к API он необязателен.
  • Ключи создаёт Owner в личном кабинете (см. API-ключи).
  • Кабинет показывает секрет подписи один раз — при создании ключа. Больше его не покажет никто, даже CardV.
  • Ключи Live начинаются с ck_live_ и cs_live_. Ключи Sandbox начинаются с ck_test_ и cs_test_. Ключ из одной среды никогда не работает в другой.
  • API начинает работать только после того, как CardV одобрит вашу компанию. После этого GET/account показывает "api_access_enabled": true.

#Заголовки

В каждом запросе, включая GET-запросы, передавайте четыре заголовка:

HTTP
X-Key-Id: ck_live_xxxxxxxxxxxxxxxxxxxxxxxx
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89
  • X-Key-Id — публичный ID вашего ключа.
  • X-Timestamp — текущее время Unix в секундах. Оно должно расходиться с часами CardV не больше чем на 5 минут (300 секунд).
  • X-Nonce — случайное значение, которое вы никогда не используете повторно. Возьмите 32 случайных шестнадцатеричных символа.
  • X-Signature — подпись, о ней рассказано в разделе Подпись запроса. Только строчные шестнадцатеричные символы.

Можно также передать X-Merchant-Id. Если вы его передаёте, он должен совпадать с ключом.

#Подпись запроса

  1. Превратите тело запроса в байты один раз. Именно эти байты вы подпишете и отправите. У GET-запроса тело пустое.

  2. Посчитайте хеш тела: BODY_HASH = SHA-256 от тела в виде строчной шестнадцатеричной строки. Для пустого тела это e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855.

  3. Соедините эти пять строк через перевод строки (\n), без перевода строки в конце:

    Text
    <METHOD>
    <PATH with query string>
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>

    METHOD пишется заглавными буквами, например GET или POST. Путь включает строку запроса точно в том виде, в каком вы её отправляете, например /api/v1/skus?limit=50.

  4. Подпишите результат: X-Signature = HMAC-SHA256 от этого текста, ключ — ваш секрет подписи. Запишите результат строчными шестнадцатеричными символами.

Запросы пополнения мобильной связи подписываются так же, но со своим путём, например /api/v1/recharge/orders.

Самая частая ошибка — подписать одну версию JSON, а отправить другую. Например, {"a":1} и {"a": 1} — это разные байты. Убедитесь, что HTTP-библиотека не меняет тело и не переставляет параметры строки запроса после подписи.

Тестовые примеры. Проверьте свой код на этих значениях. Секрет ненастоящий.

Text
Signing secret  cs_test_TEST_ONLY_not_a_real_secret_0123456789abcdef
X-Timestamp     1790000000
X-Nonce         0123456789abcdef0123456789abcdef

Запрос GET:

Text
Method       GET
Path         /api/v1/skus?limit=50
BODY_HASH    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
X-Signature  58f0362c355a6624fff9f0b84d47591c570263908832294375b7bf27731628e5

Запрос POST к /api/v1/orders с таким телом (106 байт, одна строка, без перевода строки в конце):

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.25"}]}
Text
BODY_HASH    5814ffcdadfa9eb69348150f9fd6895c00b4c37cf6cd0202b83174354645023e
X-Signature  e6e45d09ed271d8d721701b560d56a6f5104fd8a52aaad5380166f432bba6e89

#Примеры кода

Все примеры дают результаты из тестовых примеров. Загружайте секрет из хранилища секретов, а не из кода.

cURL (bash + OpenSSL)

Shell
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)

Python
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)

Node.js
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
<?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. Тело ответа выглядит так:

JSON
{"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 это делается отдельно.

  1. Создайте ключ и дайте ему название. Кабинет покажет ID ключа и секрет подписи. Сразу скопируйте секрет в хранилище секретов. Он показывается только этот один раз.
  2. Перевыпустите секрет, если вы его потеряли. ID ключа при этом не меняется. Старый секрет сразу перестаёт работать, поэтому немедленно установите новый на все свои серверы.
  3. Отключите ключ, когда он больше не нужен. Это происходит сразу и навсегда. После этого отключённый ключ можно удалить из списка. CardV сохраняет его историю.

Как сменить ключ без простоя:

  1. Создайте новый ключ и сохраните его секрет.
  2. Установите его на все свои серверы.
  3. Убедитесь в кабинете, что старый ключ больше не используется.
  4. Отключите старый ключ.

Меняйте ключи хотя бы раз в год, а также каждый раз, когда уходит сотрудник, у которого был доступ. Если ключ мог утечь, сначала отключите его, а потом разбирайтесь.

#Старые ключи (legacy)

Ключи, созданные до октября 2026 года, начинаются с cvb2b_ и используют старые заголовки X-Merchant-Id и X-Api-Key, а подписываются с ними только запросы POST. Старый ключ передаётся в каждом запросе, поэтому он легче утекает.

Live и Sandbox больше не принимают такие ключи. Перейдите на новый ключ уже сейчас: создайте его в кабинете, переведите код на заголовки выше, а затем отключите старый ключ.

Вопросы по интеграции? Напишите на [email protected], указав ваш Merchant ID и ID заказа или запроса.