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

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

В каждом запросе к API передаются ваш Merchant ID и API-ключ. Запрос на оформление заказа (POST/orders) ещё и подписывается — так никто не сможет изменить или повторить его.

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

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

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

#Заголовки

В каждом запросе передавайте два заголовка:

HTTP
X-Merchant-Id: M00000001
X-Api-Key: cvb2b_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

В POST/orders добавьте ещё три:

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

GET-запросы подписывать не нужно.

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

  1. Превратите заказ в текст JSON один раз. Именно эти байты вы подпишете и отправите.

  2. Посчитайте хеш тела: BODY_HASH = SHA-256 от тела в виде строчной шестнадцатеричной строки.

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

    Text
    POST
    /api/v1/orders
    <X-Timestamp>
    <X-Nonce>
    <BODY_HASH>
  4. Подпишите результат: X-Signature = HMAC-SHA256 от этого текста, ключ — ваш полный API-ключ. Запишите результат строчными шестнадцатеричными символами.

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

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

Text
API-ключ     cvb2b_TEST_ONLY_not_a_real_key_0123456789abcdef
Метод        POST
Путь         /api/v1/orders
X-Timestamp  1790000000
X-Nonce      0123456789abcdef0123456789abcdef

Тело (108 байт, одна строка, без перевода строки в конце):

JSON
{"external_order_id":"TEST-0001","items":[{"sku_id":"S000001","quantity":1,"expected_unit_price":"9.2500"}]}

Ожидаемый результат:

Text
BODY_HASH    b0545ae25d54b219f27d8bd90e4dcbf26cf0d491f53982da67b8eef0a1a59960
X-Signature  fd55fa31a3b6817d5368903dbb4bad4458381e024c17e688580bac96577acfed

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

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

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

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

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

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

  1. Создайте ключ и дайте ему название. Сам ключ кабинет пока не показывает.
  2. Откройте его (Reveal). CardV пришлёт на почту 6-значный код, он действует 10 минут (5 попыток). Введите код, чтобы увидеть ключ целиком. Сразу сохраните его. Каждое открытие записывается в журнал.
  3. Отключите ключ, когда он больше не нужен. Это происходит сразу и навсегда.

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

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

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

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