Разработчикам/Начало работы
Аутентификация
В каждом запросе к API передаются ваш Merchant ID и API-ключ.
Запрос на оформление заказа (POST/orders) ещё и подписывается — так никто не сможет изменить или повторить его.
См. также: Каталог и заказы · Безопасность · README
#Учётные данные
| Что | Пример | Секрет? |
|---|---|---|
| Merchant ID | M00000001 | Нет. Он лишь говорит, кто вы. |
| API-ключ | cvb2b_... | Да. Это и ваш пароль, и ключ для подписи. |
- Merchant ID вы получаете от CardV, когда ваш аккаунт одобрен.
- API-ключ создаёт Owner в личном кабинете (см. API-ключи).
- У Live и Sandbox разные ключи. Ключ из одной среды никогда не работает в другой.
- API начинает работать только после того, как CardV одобрит вашу компанию.
После этого
GET/accountпоказывает"api_access_enabled": true.
#Заголовки
В каждом запросе передавайте два заголовка:
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxВ POST/orders добавьте ещё три:
X-Timestamp: 1790000000
X-Nonce: 0123456789abcdef0123456789abcdef
X-Signature: fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfedX-Timestamp— текущее время Unix в секундах. Оно должно расходиться с часами CardV не больше чем на 5 минут (300 секунд).X-Nonce— случайное значение, которое вы никогда не используете повторно. Возьмите 32 случайных шестнадцатеричных символа.X-Signature— подпись, о ней рассказано в разделе Подпись заказа. Только строчные шестнадцатеричные символы.
GET-запросы подписывать не нужно.
#Подпись заказа
Превратите заказ в текст JSON один раз. Именно эти байты вы подпишете и отправите.
Посчитайте хеш тела:
BODY_HASH= SHA-256 от тела в виде строчной шестнадцатеричной строки.Соедините эти пять строк через перевод строки (
\n), без перевода строки в конце:POST /api/v1/orders <X-Timestamp> <X-Nonce> <BODY_HASH>Подпишите результат:
X-Signature= HMAC-SHA256 от этого текста, ключ — ваш полный API-ключ. Запишите результат строчными шестнадцатеричными символами.
Самая частая ошибка — подписать одну версию JSON, а отправить другую.
Например, {"a":1} и {"a": 1} — это разные байты.
Убедитесь, что HTTP-библиотека не меняет тело после подписи.
Тестовый пример. Проверьте свой код на этих значениях. Ключ ненастоящий.
API-ключ cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Метод POST
Путь /api/v1/orders
X-Timestamp 1790000000
X-Nonce 0123456789abcdef0123456789abcdefТело (108 байт, одна строка, без перевода строки в конце):
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}Ожидаемый результат:
BODY_HASH b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed#Примеры кода
Все примеры дают результат из тестового примера. Загружайте ключ из хранилища секретов, а не из кода.
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.2500"}]}'
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_API_KEY" -hex | sed 's/^.*= //')
curl -sS -X POST "$BASE_URL$REQ_PATH" \
-H "Content-Type: application/json" \
-H "X-Merchant-Id: $CARDV_MERCHANT_ID" \
-H "X-Api-Key: $CARDV_API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Nonce: $NONCE" \
-H "X-Signature: $SIG" \
--data-raw "$BODY"Используйте --data-raw, а не -d @file. -d удаляет переводы строк, и отправленные байты не совпадут с подписанными.
Python (requests)
import hashlib, hmac, json, os, secrets, time
import requests
BASE_URL = "https://sandbox.cardv.net"
MERCHANT_ID = os.environ["CARDV_MERCHANT_ID"]
API_KEY = os.environ["CARDV_API_KEY"]
AUTH = {"X-Merchant-Id": MERCHANT_ID, "X-Api-Key": API_KEY}
def sign(path: str, body: bytes, ts: str, nonce: str) -> str:
body_hash = hashlib.sha256(body).hexdigest()
text = "\n".join(["POST", path, ts, nonce, body_hash])
return hmac.new(API_KEY.encode(), text.encode(), hashlib.sha256).hexdigest()
def cardv_get(path: str, params=None) -> requests.Response:
return requests.get(BASE_URL + path, params=params, headers=AUTH, timeout=30)
def cardv_post(path: str, payload: dict) -> requests.Response:
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode()
ts, nonce = str(int(time.time())), secrets.token_hex(16)
headers = {
**AUTH,
"Content-Type": "application/json",
"X-Timestamp": ts,
"X-Nonce": nonce,
"X-Signature": sign(path, body, ts, nonce),
}
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.2500"}],
})
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_MERCHANT_ID, CARDV_API_KEY } = process.env;
const AUTH = { "X-Merchant-Id": CARDV_MERCHANT_ID, "X-Api-Key": CARDV_API_KEY };
function sign(path, body, timestamp, nonce) {
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const text = ["POST", path, timestamp, nonce, bodyHash].join("\n");
return crypto.createHmac("sha256", CARDV_API_KEY).update(text, "utf8").digest("hex");
}
async function cardvPost(path, payload) {
const body = Buffer.from(JSON.stringify(payload), "utf8"); // serialize once
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomBytes(16).toString("hex");
const headers = {
...AUTH,
"Content-Type": "application/json",
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Signature": sign(path, body, timestamp, nonce),
};
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.2500" }],
}));PHP (только подпись)
<?php
function cardv_sign(
string $apiKey, string $path, string $body, string $ts, string $nonce
): string {
$text = implode("\n", ['POST', $path, $ts, $nonce, hash('sha256', $body)]);
return hash_hmac('sha256', $text, $apiKey); // lowercase hex
}
// Send exactly $body.
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$nonce = bin2hex(random_bytes(16));
$signature = cardv_sign($apiKey, '/api/v1/orders', $body, $ts, $nonce);#Ошибки
Все эти ошибки возвращают HTTP 403 (не 401). Исключение — превышение лимита запросов: для него приходит 429. Тело ответа выглядит так:
{"detail": "Invalid HMAC signature."}| Проблема | Что делать |
|---|---|
| Нет Merchant ID или API-ключа, либо они неверны | Проверьте оба заголовка и среду. |
| Доступ к 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 это делается отдельно.
- Создайте ключ и дайте ему название. Сам ключ кабинет пока не показывает.
- Откройте его (Reveal). CardV пришлёт на почту 6-значный код, он действует 10 минут (5 попыток). Введите код, чтобы увидеть ключ целиком. Сразу сохраните его. Каждое открытие записывается в журнал.
- Отключите ключ, когда он больше не нужен. Это происходит сразу и навсегда.
Как сменить ключ без простоя:
- Создайте и откройте новый ключ.
- Установите его на все свои серверы.
- Убедитесь в кабинете, что старый ключ больше не используется.
- Отключите старый ключ.
Меняйте ключи хотя бы раз в год, а также каждый раз, когда уходит сотрудник, у которого был доступ. Если ключ мог утечь, сначала отключите его, а потом разбирайтесь.
Вопросы по интеграции? Напишите на [email protected], указав ваш Merchant ID и ID заказа или запроса.