Разработчикам/Интеграция

Каталог и заказы

Здесь по шагам описан весь процесс покупки: проверить баланс, найти товар, узнать цену, оформить заказ и получить коды.

См. также: Аутентификация · Общие правила · Вебхуки

#Аккаунт и баланс

#Аккаунт

GET/api/v1/account показывает данные вашей компании и то, включён ли доступ к API.

JSON
{
  "merchant_id": "M00000001",
  "name": "Acme Shop",
  "legal_name": "Acme Shop Ltd",
  "billing_email": "[email protected]",
  "status": "active",
  "kyb_status": "approved",
  "api_access_enabled": true,
  "default_currency": "USD",
  "purchase_limits": {
    "currency": "USD",
    "verified": true,
    "max_order_amount": "1000.00",
    "daily_order_amount_limit": "10000.00",
    "daily_order_count_limit": 1000,
    "used_today_amount": "120.50",
    "used_today_count": 3,
    "day_resets_at": "2026-10-01T00:00:00+00:00"
  }
}
  • default_currency — валюта вашего кошелька. Все цены, которые вы платите, указаны в ней.
  • api_access_enabled становится true, когда CardV одобрит вашу компанию.

#Баланс

GET/api/v1/balance показывает, сколько денег вы можете потратить.

JSON
{
  "currency": "USD",
  "balance": "1520.40",
  "reserved_amount": "0.00",
  "available_balance": "1520.40",
  "low_balance_threshold": "200.00",
  "low_balance_notified_at": null,
  "is_active": true
}
  • available_balance — сумма, которую можно потратить прямо сейчас. Это balance минус reserved_amount.
  • Заказ на сумму больше available_balance отклоняется, деньги не списываются.
  • low_balance_threshold — порог, при котором приходит письмо о низком балансе. Он задаётся в кабинете.
  • Пополнить кошелёк можно в кабинете.

#Курс валют

GET/api/v1/fx-rates?quote=EUR возвращает курс USD к другой валюте.

Используйте его, если ваш магазин продаёт не в USD. CardV всегда списывает средства с кошелька в USD. Курс нужен только для того, чтобы вы сами задавали цены.

JSON
{
  "base": "USD",
  "quote": "EUR",
  "rate": "0.9234",
  "as_of": "2026-10-11T07:05:00Z"
}
  • quote — код валюты ISO 4217, например EUR, GBP или JPY.
  • base необязателен. Поддерживается только USD.
  • rate — сколько единиц quote вы получаете за 1 USD. Это десятичная строка. Не читайте её как float.
  • as_of — когда CardV последний раз обновил этот курс. Курсы обновляются каждый час.
  • quote=USD возвращает "rate": "1".
  • Сохраняйте курс у себя. Достаточно запрашивать его несколько раз в день.
СтатусЗначениеЧто делать
400Нет quote, это не трёхбуквенный код, или base не USDИсправьте запрос
404No exchange rate for this currency.Задайте цены в этой валюте сами
503Exchange rate temporarily unavailable.Продолжайте использовать последний сохранённый курс и повторите позже

Подписывайте этот запрос, как и любой другой. Подписываемый путь включает строку запроса, например /api/v1/fx-rates?quote=EUR.

#Товары (SKU)

SKU — это конкретный товар, который можно купить, например «Steam Wallet 10 USD». Его ID выглядит так: S000456. Цену запрашивают и заказ оформляют именно по SKU.

#Модели товаров

Каждый SKU относится к одной из этих моделей. Различайте их по denomination_type и product_type.

МодельКак распознатьЦенаЗаказЧто вы получаете
Фиксированный номиналdenomination_type: "fixed"merchant_price — цена одной единицы.Передайте quantity.Коды в deliveries.
Диапазонdenomination_type: "range"От merchant_price_min до merchant_price_max. Реальная цена зависит от суммы: запросите котировку.Передайте quantity и amount.Коды в deliveries.
Прямое пополнениеproduct_type: "direct_charge"Как у фиксированного номинала.Передайте также inputs (см. прямое пополнение).Обычно без кодов. succeeded означает, что аккаунт пополнен.
Пополнение мобильной связиНет в этом каталогеСвоя котировкаСвой APIСм. Пополнение мобильной связи.
  • vertical — это линейка товаров, а не модель. У прямого пополнения тоже может быть vertical: "gift_card".
  • Любой SKU с фиксированным номиналом или диапазоном может выдаваться как ссылка для активации CardV вместо кода. Тогда у SKU delivery_mode: "voucher". Вы получаете ссылку (delivery_type: "link", ссылка в redeem_url). Клиент открывает её и получает код. Передайте её клиенту так же, как код.

GET/api/v1/skus возвращает список товаров, которые вам доступны. Пример:

HTTP
GET /api/v1/skus?search=steam&region=US&limit=50&offset=0

Фильтры (все необязательные):

ФильтрПримерЧто ищет
searchsteamID товара, название или бренд
brandSteamНазвание бренда (регистр не важен)
regionUSКод или название страны. GLC — весь мир, EU — Европа
categoryTravelБизнес-категория, например Travel, Digital Wallets & Payment, Game Credits
verticalgift_cardТоварное направление
product_typepin_codeСпособ выдачи

Постраничная выдача: передайте limit (по умолчанию 100, максимум 500) и offset. Увеличивайте offset и запрашивайте снова, пока offset не дойдёт до count.

JSON
{
  "count": 7,
  "limit": 2,
  "results": [
    {
      "sku_id": "S000456",
      "product_id": "P000123",
      "name": "Steam Wallet 10 USD",
      "product_name": "Steam Wallet US",
      "brand": "Steam",
      "region": "US",
      "vertical": "gift_card",
      "product_type": "pin_code",
      "denomination_type": "fixed",
      "denomination_value": "10",
      "face_currency": "USD",
      "merchant_price": "9.25",
      "merchant_price_min": "9.25",
      "merchant_price_max": "9.25",
      "settlement_currency": "USD",
      "availability": "available",
      "min_quantity": 1,
      "max_quantity": 100,
      "required_input_schema": [],
      "...": "more fields"
    },
    {
      "sku_id": "S006126",
      "product_id": "P002210",
      "name": "Apple ES",
      "product_name": "Apple ES",
      "brand": "Apple",
      "region": "ES",
      "vertical": "gift_card",
      "product_type": "pin_code",
      "denomination_type": "range",
      "denomination_value": "0",
      "min_face_value": "2",
      "max_face_value": "250",
      "amount_step": null,
      "face_currency": "EUR",
      "merchant_price": "2.28",
      "merchant_price_min": "2.28",
      "merchant_price_max": "284.15",
      "price_label": "From 2.28 USD",
      "settlement_currency": "USD",
      "availability": "available",
      "...": "more fields"
    }
  ],
  "filter_options": {"brands": [], "regions": [], "categories": [], "verticals": []}
}

GET/api/v1/skus/{sku_id} возвращает один товар с теми же полями.

Самые полезные поля:

ПолеЧто значит
sku_idID, по которому запрашивают цену и оформляют заказ.
merchant_priceВаша цена за единицу в settlement_currency. Для SKU range это только цена суммы min_face_value, начальная цена. Не используйте её для других сумм: запросите котировку с amount.
availabilityavailable или unavailable. Заказывайте только available.
denomination_typefixed или range. См. фиксированный номинал и диапазон.
denomination_valueНоминал SKU fixed. Для SKU range — "0".
min_face_value, max_face_valueSKU range: минимальная и максимальная сумма, которую может выбрать клиент, в face_currency.
merchant_price_min, merchant_price_maxЦена минимальной и максимальной суммы. Для SKU fixed равны merchant_price. Используйте их, чтобы показать диапазон цен; перед заказом всегда запрашивайте котировку точной суммы. merchant_price_max равен "", если сейчас его нельзя рассчитать.
price_labelmerchant_price в виде текста для показа. У SKU range начинается с From, например From 2.28 USD.
face_value_labelНоминал в виде текста для показа, например 10 USD или 50 - 1000 GBP.
discountСкидка от номинала, например 7.50% off face value. Только для SKU fixed, у которых валюта карты совпадает с валютой кошелька; иначе "".
amount_stepТолько для товаров с диапазоном. Сумма должна быть кратна этому значению, например "1" означает только целые числа. null означает, что шага нет.
face_currencyВалюта, указанная на карте. Может отличаться от валюты кошелька.
min_quantity, max_quantityСколько единиц может быть в одной строке заказа.
product_typepin_code (вы получаете код) или direct_charge (мы пополняем аккаунт).
delivery_typeЧто выдаёт поставщик: code, code_pin, code_serial, url или direct_charge. Если delivery_mode равен voucher, вместо этого вы получаете ссылку.
delivery_modecode (вы получаете сам код) или voucher (вы получаете ссылку для активации CardV).
voucher_instructionsДля SKU voucher: текст, который объясняет клиенту, как пользоваться ссылкой. Иначе "".
vertical, categoryЛинейка товаров и бизнес-категория, для фильтров и показа.
region, region_name, region_labelКод страны или региона, его название и подпись для показа.
required_input_schemaДанные, которые нужно передать для прямого пополнения.
brand_logo_url, image_urlКартинки на серверах CardV или "".
description, redemption_instructions, terms, disclaimerТексты, которые можно показать покупателям.

Советы:

  • Вы видите только активные товары, открытые для вашего аккаунта. Для остальных придёт 404.
  • Обновляйте список товаров каждые 5–15 минут. Прямо перед заказом всегда запрашивайте цену.
  • Показывайте клиентам цены из синхронизированного списка. Не вызывайте запрос цены при каждом просмотре страницы или для каждого посетителя.
  • Цены SKU с диапазоном следуют за стоимостью у поставщика и могут меняться несколько раз в день. Обновляйте merchant_price_min и merchant_price_max вместе с синхронизацией списка товаров.
  • filter_options перечисляет бренды, регионы и направления, по которым можно фильтровать.

#Синхронизация по товарам

Если в вашем магазине одна страница на товар с выбором номинала, синхронизируйте товары, а не SKU. GET/api/v1/products возвращает список товаров. В каждом товаре есть все SKU, которые вы можете в нём купить.

HTTP
GET /api/v1/products?brand=Steam&region=GB&limit=50&offset=0

Фильтры и постраничный вывод те же, что у GET/api/v1/skus. count — число товаров, sku_count — число их SKU.

JSON
{
  "count": 1,
  "sku_count": 4,
  "limit": 50,
  "results": [
    {
      "product_id": "P000123",
      "product_name": "Steam Wallet UK",
      "brand": "Steam",
      "category": "Gaming",
      "region": "GB",
      "product_type": "pin_code",
      "image_url": "https://b2b.cardv.net/api/v1/catalog-assets/steam.png",
      "required_input_schema": [],
      "skus": [
        {
          "sku_id": "S000456",
          "name": "Steam Wallet 5 GBP",
          "label": "5 GBP",
          "denomination_type": "fixed",
          "denomination_value": "5",
          "face_currency": "GBP",
          "merchant_price": "7.61",
          "settlement_currency": "USD",
          "availability": "available",
          "min_quantity": 1,
          "max_quantity": 100,
          "...": "more fields"
        }
      ]
    }
  ],
  "filter_options": {"brands": [], "regions": [], "categories": [], "verticals": []}
}

GET/api/v1/products/{product_id} возвращает один товар с его SKU, а также description, redemption_instructions, terms и disclaimer.

  • У каждого SKU в skus те же поля и значения, что и в GET/api/v1/skus. Расчёт цены и заказ — как обычно, по sku_id.
  • Для текста варианта используйте label, например 5 GBP или Tinder Plus 1 Month. Некоторые товары — это тарифы, а не номиналы.
  • У товара одна валюта номинала и один тип суммы. SKU с произвольной суммой всегда выделен в отдельный товар.
  • В списке нет длинных текстов. Получите их один раз для каждого товара через GET/api/v1/products/{product_id}.

#Фиксированный номинал и диапазон

У большинства товаров фиксированный номинал, например 10 USD. У некоторых номинал задаётся диапазоном: покупатель сам выбирает сумму, например от 5 до 500 USD.

ТипПри запросе ценыПри заказе
fixedПередайте quantityНе передавайте amount
rangeПередайте quantity и amountПередайте amount

Для товара с диапазоном amount должен быть между min_face_value и max_face_value. Сумма указывается в face_currency.

Цена SKU с диапазоном. В списках SKU и товаров merchant_price и price_label — это только цена суммы min_face_value. merchant_price_min и merchant_price_max дают цену минимальной и максимальной суммы. Цена промежуточной суммы не является простым кратным ни одной из них. Прежде чем назначать свою цену продажи или заказывать, запросите котировку с точной amount и передайте её merchant_price как expected_unit_price.

У некоторых товаров с диапазоном есть ещё amount_step. Если он не null, amount должен быть ему кратен. Например, при "amount_step": "1" можно отправить 25, но нельзя 25.50. Запрос цены или заказ с суммой, не подходящей под шаг, вернёт HTTP 400, деньги не списываются.

#Прямое пополнение

Некоторые товары пополняют аккаунт вашего покупателя напрямую, например игровой аккаунт. Для этого CardV нужны данные аккаунта. Какие именно — указано в товаре:

JSON
"required_input_schema": [
  {"key": "player_id", "label": "Player ID", "type": "text", "required": true},
  {"key": "server", "label": "Server", "type": "text", "required": false}
]

Передайте значения в поле inputs строки заказа, используя каждый key:

JSON
"inputs": {"player_id": "123456789", "server": "EU"}
  • Поле обязательно, если у него не указано "required": false. type — подсказка для поля вашей формы, например text.
  • Если обязательного значения нет, заказ отклоняется с ошибкой items.
  • Это персональные данные вашего покупателя. Защищайте их (см. Безопасность).

#Запрос цены

Запрос цены показывает текущую цену за нужное количество.

HTTP
GET /api/v1/skus/S000456/quote?quantity=2
GET /api/v1/skus/S000789/quote?quantity=1&amount=25.00
JSON
{
  "sku_id": "S000456",
  "settlement_currency": "USD",
  "merchant_price": "9.25",
  "quantity": 2,
  "total_price": "18.50",
  "min_quantity": 1,
  "max_quantity": 100,
  "availability": "available"
}
  • Запрос цены не закрепляет цену. Цены могут измениться в любой момент.
  • Запрашивайте цену один раз, когда клиент оформляет покупку, с коротким тайм-аутом, например 5 секунд. Если запрос не уложился в тайм-аут, продолжайте показывать синхронизированную цену и повторите попытку; никогда не показывайте страницу ошибки из-за того, что запрос цены был медленным.
  • Чтобы подстраховаться, при заказе передайте merchant_price в поле expected_unit_price. Передавайте цену из запроса цены, а не рассчитанную самостоятельно: она должна совпадать точно, до цента. Если цена изменилась, CardV отклонит заказ и ничего не спишет.
  • Количество или сумма вне допустимого диапазона, а также сумма, не кратная amount_step, вернёт HTTP 400 с ошибкой quantity или amount.

#Оформление заказа

POST/api/v1/orders покупает один или несколько товаров и оплачивает их из кошелька. Как и любой запрос, его нужно подписать.

JSON
{
  "external_order_id": "SHOP-20260929-10001",
  "items": [
    {"sku_id": "S000456", "quantity": 2, "expected_unit_price": "9.25"},
    {"sku_id": "S000789", "amount": "25.00", "expected_unit_price": "23.75"},
    {
      "sku_id": "S000900",
      "expected_unit_price": "4.90",
      "inputs": {"player_id": "123456789"}
    }
  ]
}
ПолеОбязательноЧто значит
external_order_idДаВаш номер заказа, от 1 до 120 символов. Должен быть уникальным.
itemsДаОдна или несколько строк заказа.
items[].sku_idДаТовар, который вы покупаете.
items[].quantityНетСколько штук. По умолчанию 1.
items[].amountДля товаров с диапазономНоминал, который вы покупаете. Должен соответствовать min_face_value, max_face_value и amount_step.
items[].expected_unit_priceЖелательноmerchant_price из запроса цены. Передавайте всегда.
items[].inputsДля прямого пополненияДанные аккаунта для прямого пополнения.

Приняв заказ, CardV сразу списывает с кошелька всю сумму. Затем CardV в фоновом режиме начинает выдачу кодов.

Ответ приходит с HTTP 201:

JSON
{
  "idempotent_replay": false,
  "order": {
    "order_id": "O-00001234",
    "external_order_id": "SHOP-20260929-10001",
    "status": "accepted",
    "total_amount": "46.20",
    "...": "more fields"
  }
}

Сохраните order.order_id. Кодов в этом ответе нет никогда — их нужно получить позже (см. Получение заказа).

#Отклонённые заказы

Отклонённый заказ возвращает HTTP 400, деньги не списываются. Причину подскажет ключ ошибки:

КлючПричинаЧто делать
itemsИзменилась цена, товар недоступен, неверная сумма или не хватает данныхЗапросите цену заново, исправьте и отправьте снова
balanceВ кошельке не хватает денегПополните кошелёк в кабинете
riskПревышен лимит на размер заказа или дневной лимитСвяжитесь с CardV
external_order_idВаш номер заказа уже занят другим заказомСм. Безопасный повтор

Пример ошибки из-за изменения цены:

JSON
{
  "items": "SKU S000456 price changed from 9.25 to 9.41 USD; refresh quote and confirm again."
}

У вашего аккаунта есть лимиты на один заказ, на сумму покупок за день и на число заказов за день. GET/api/v1/account показывает их в purchase_limits вместе с тем, сколько уже использовано сегодня. 0 означает «без лимита». Дневные лимиты сбрасываются в 00:00 UTC.

#Безопасный повтор

Ваш номер заказа (external_order_id) защищает от двойной покупки. Если вы отправите тот же заказ с тем же номером ещё раз, CardV не спишет деньги повторно, а вернёт уже существующий заказ.

Что вы отправляетеЧто получаете
Новый номер заказаHTTP 201. Новый заказ. Деньги списываются с кошелька.
Тот же номер и тот же заказHTTP 200 и "idempotent_replay": true. Существующий заказ. Без списания.
Тот же номер, но другой заказHTTP 400 с ошибкой external_order_id. Ничего не происходит.

«Тот же заказ» — это те же строки в том же порядке, с теми же товарами, количеством, суммой и данными inputs. Если вы передаёте expected_unit_price, он должен совпадать с ценой из первого заказа.

Повторный заказ распознаётся ещё до проверки баланса и цены. Поэтому всегда возвращается первый заказ, даже если с тех пор цена изменилась.

#Как повторять без риска

Если вы не получили понятного ответа, просто отправьте тот же заказ ещё раз.

Text
POST /orders с номером заказа R
 ├─ 201 или 200 → сохраните order_id. Готово.
 ├─ 400 items / balance / risk → заказ не создан.
 │      Устраните причину и отправьте снова. R можно оставить.
 ├─ 400 external_order_id → R уже занят другим заказом. Остановитесь и разберитесь.
 ├─ 403 ошибка подписи → подпишите заново и отправьте то же тело.
 ├─ 429 → дождитесь Retry-After, подпишите заново, отправьте то же тело.
 └─ тайм-аут, 5xx или обрыв соединения
        → отправьте то же тело с тем же R.
          Придёт 201 (первая попытка не дошла) или 200 (дошла).

Правила:

  • Никогда не придумывайте новый номер заказа из-за того, что ответ потерялся. Если первый запрос всё-таки дошёл, с новым номером вы купите всё дважды.
  • Для каждой повторной отправки нужны новые время, nonce и подпись. Тело остаётся прежним.

#Получение заказа

GET/api/v1/orders/{order_id} возвращает заказ, его статус и коды.

JSON
{
  "order_id": "O-00001234",
  "external_order_id": "SHOP-20260929-10001",
  "status": "succeeded",
  "currency": "USD",
  "total_amount": "18.50",
  "created_at": "2026-09-29T08:15:30.123456Z",
  "updated_at": "2026-09-29T08:15:41.004211Z",
  "items": [
    {
      "sku_id": "S000456",
      "product_name": "Steam Wallet US",
      "quantity": 2,
      "unit_price": "9.25",
      "total_price": "18.50",
      "delivery_count": 2,
      "deliveries": [{"...": "see Codes below"}]
    }
  ],
  "...": "more fields"
}
  • total_amount — сумма, списанная при приёме заказа.
  • items[].unit_price — цена, закреплённая за этим заказом.
  • В items[].deliveries лежат коды целиком. Обращайтесь с ответом как с секретом.
  • invoice_url и delivery_file_url — пути к счёту и к кодам в формате CSV. Оба работают только в кабинете. С API-ключом они вернут HTTP 403.
  • Ещё в ответе есть id (старый номер, не используйте его), events (история только для показа) и ход выдачи по каждой строке. Всё это можно не учитывать.
  • Для неизвестного номера заказа придёт HTTP 404.

#Статус заказа и коды

#Статус

Text
accepted ──► processing ──► succeeded
                  │
                  ├──► partially_succeeded   (часть строк выдана, часть нет)
                  │
                  └──► failed ──► refunded   (деньги вернулись в кошелёк)
СтатусЗавершён?Что делать
acceptedНетЖдать. Деньги списаны, выдача ещё не началась.
processingНетЖдать. Не оформляйте заказ повторно.
succeededДаПолучите коды и передайте их покупателю.
partially_succeededДаВыдайте то, что пришло. За остальное деньги вернутся позже.
failedЕщё нетДождитесь refunded. Статус failed — это ещё не возврат денег.
refundedДаДеньги вернулись в кошелёк.

Если вы не используете вебхуки, опрашивайте так: через 5 секунд, затем через 10 с, 30 с, 60 с, а дальше каждые 5 минут. Не выходите за лимит запросов. Большинство заказов завершается за секунды. Некоторым нужна ручная проверка, и она может занять несколько часов.

#Коды

Каждая выданная единица — отдельный объект в items[].deliveries:

JSON
{
  "status": "stored",
  "delivery_type": "card_pin",
  "card_number": "X1234",
  "pin_code": "9876",
  "redeem_url": "",
  "expiry_date": "2027-09-29",
  "instructions": "Redeem at ..."
}
ПолеЧто это
card_numberОсновной код: номер карты, код ваучера или PIN.
pin_codeВторой код, например PIN или защитный код. Пустое, если второго кода нет.
redeem_urlСсылка для активации. Для выдачи типа link сама ссылка и есть код.
expiry_dateПоследний день, когда код можно использовать, в формате YYYY-MM-DD. "" означает, что поставщик не указал срок действия. Это не значит, что срок действия кода истёк.
  • Передайте покупателю все непустые поля: card_number, pin_code, redeem_url, expiry_date и instructions.
  • delivery_type показывает, что именно вы получили: code, card_pin, link, code_link или qr.
  • Держите redeem_url в секрете, как и код.
  • Никогда не удаляйте, не скрывайте и не аннулируйте код из-за того, что expiry_date пуст. Показывайте клиенту срок действия, только если поле не пустое.
  • Никогда не передавайте покупателю единицу со status равным voided.
  • У товаров с прямым пополнением выдачи обычно нет. succeeded значит, что аккаунт пополнен.
  • Кодов в вебхуках никогда нет. Получив вебхук, запросите заказ.

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