Platina API: первый заказ
Platina API · v1

Первый заказ за 15 минут

Platina отдаёт вашему магазину пополнения Steam, Telegram Stars, игровую валюту и гифт-карты по одному API. Вы держите рублёвый баланс, мы выбираем поставщика, выдаём код или зачисляем на аккаунт и присылаем вебхук. Ниже путь от тестового ключа до первого успешного заказа в песочнице.

1. Ключтестовый pk_test_…
2. КаталогGET /v1/catalog
3. ЗаказPOST /v1/orders
4. Результатstatus + code
5. Вебхукorder.updated
Термины. Песочница (тест) — ключ pk_test_, тестовый баланс, заказы ненастоящие. Бой — ключ pk_live_, настоящие деньги и заказы. Права ключа (в API — scope): read «Чтение», orders «Заказы», topups «Пополнения», payments «Оплаты покупателей». Готовность к бою — 14 проверок интеграции в кабинете; подсказка, а не допуск: боевой ключ можно выпустить сразу. Номер запроса — request_id, он же заголовок X-Request-Id.

1. Ключ и песочница

Ключ выпускаете вы сами в кабинете, на странице «Ключи API». Тестовый ключ создаётся при регистрации и показывается один раз на «Ключах API»; не успели сохранить — выпустите новый там же. Адрес API — в строке «Адрес API» на странице «API и тестер» и в готовом curl под новым ключом (в примерах ниже он условный). Тестовый начинается с pk_test_, боевой с pk_live_. Передавайте его в заголовке Authorization: Bearer <ключ>. Ключ показывается один раз, у нас хранится только хеш.

Тестовый ключ видит только поставщика «песочница»: заказы не исполняются по-настоящему, баланс песочницы отдельный и пополняется бесплатно. Проверить ключ:

curl https://api.platina.example/v1/me \
  -H "Authorization: Bearer pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{"partner":{"slug":"demo-shop","brand":"Demo Shop","tariff":"start","status":"active"},
 "key":{"prefix":"pk_test_xxxx","sandbox":true,"scopes":["read","orders","topups"]}}
ScopeЧто разрешает
readкаталог, курс, заказы и баланс на чтение. Кода товара в ответе нет: вместо него "code": null и "code_hidden": "scope_orders_required"
ordersсоздание заказов с баланса, коды товаров в карточке заказа, операции с картами (в том числе PAN, CVV и коды 3-DS), настройка вебхуков. Ключ, которым вы опрашиваете заказы ради кода, должен иметь это право
topupsзаявки на пополнение
paymentsприём оплаты от ваших покупателей (доп. модуль, раздел 6b)

Кабинет добавляет read к любому ключу с orders, topups или payments: без чтения ключ не увидел бы статус своего же заказа, платежа или заявки.

Лимит 10 запросов в секунду на ключ, всплеск до 20. К ключу можно привязать список IP-адресов, тогда с других адресов придёт 403.

2. Каталог и цена

GET/v1/catalog?category=&region=&include_unavailable=

Баланс и цены списания — в USDT, уже с вашей долей тарифа, без курса: закупка в $ × (1 + доля). У товаров с фиксированным номиналом (коды, пакеты валюты) поле price_usdt за штуку — ровно столько спишется; у товаров с произвольной суммой (пополнение Steam) поле price_usdt_per_usd за 1 $ номинала (4 знака, сумма заказа округляется вверх до цента). Рядом — цена покупателя с вашими наценками из кабинета и округлением, как в боте и витрине, в рублях: retail_rub (за штуку) или retail_rub_per_usd, и сама наценка markup_pct; price_mode: "fixed" — вместо наценки вы задали товару фиксированную цену в рублях, она и стоит в retail_rub; "market" — цена «По рынку»: на 1 ₽ дешевле самой низкой цены на площадках, в границах вашей наценки. hidden: true — вы скрыли товар из бота и витрины, по API он заказывается как обычно. По умолчанию в ответе только то, что есть у поставщиков; с include_unavailable=true придут и остальные — с available: false и без цен. Узнавать об изменениях сразу — событие catalog.updated.

{"sandbox":true,"tariff":"start",
 "fx":{"rate":"82.0000","buy_rate":"82.4100","stale":false,"fetched_at":"2026-10-17T12:00:00+00:00"},
 "items":[
  {"id":"6f1c…","code":"steam_topup_ru","category":"steam_topup","name":"Пополнение Steam (RU)",
   "region":"RU","recipient_type":"steam_login","amount_min_usd":"1.0000","amount_max_usd":"500.0000",
   "price_usdt_per_usd":"0.9741","retail_rub_per_usd":"86.75","markup_pct":"8","take_pct":"1.000","suppliers":1,
   "available":true,"hidden":false},
  {"id":"9a2d…","code":"apple_us_10","category":"voucher","name":"Apple Gift Card $10 (US)",
   "region":"US","recipient_type":"none","nominal_usd":"10.0000","price_usdt":"9.99","retail_rub":"889.00","markup_pct":"8","take_pct":"4.000",
   "suppliers":1,"available":true,"hidden":false}
 ]}

recipient_type говорит, что спросить у покупателя: steam_login → логин Steam, username → ник Telegram, uid → игровой ID (иногда с сервером), none → ничего, вы получите код.

Курс USDT/RUB — в GET /v1/rates (Rapira; buy_rate уже включает наш запас 0,5 %). На списание заказов он не влияет: по нему считаются розничные цены в рублях и зачисляются рублёвые пополнения и оплаты покупателей.

Там же курс кошелька Steam — сколько рублей или тенге зачислится на аккаунт за 1 $ amount_usd: "steam_wallet":[{"currency":"RUB","per_usd":"79.85","source":"playwallet","fetched_at":"…","stale":false}]. Покупатель хочет пополнить на 1000 ₽ — передайте amount_usd = 1000 / per_usd, округлив до цента, и покажите «зачислится ≈ 1000 ₽»: точную сумму считает Steam в момент зачисления. Курс сверяется раз в час, stale: true — ему больше двух суток.

3. Первый заказ

POST/v1/orders

Передайте свой идентификатор заказа в client_order_id. Повтор запроса с тем же идентификатором вернёт тот же заказ со статусом 200, а не создаст второй. Так безопасно повторять запрос при обрыве сети.

curl -X POST https://api.platina.example/v1/orders \
  -H "Authorization: Bearer pk_test_…" -H "Content-Type: application/json" \
  -d '{"client_order_id":"shop-10231","product":"steam_topup_ru","amount_usd":"10","recipient":{"login":"gaben"}}'
HTTP 201
{"id":"5b2f…","client_order_id":"shop-10231","status":"held","sandbox":true,
 "product":"steam_topup_ru","amount_usd":"10.00","qty":1,"recipient":{"login":"gaben"},
 "price_usdt":"10.10","error":null,
 "created_at":"2026-10-17T12:01:05+00:00","updated_at":"…","delivered_at":null}

Для товаров с номиналом вместо amount_usd передайте qty (от 1 до 10 000): {"client_order_id":"shop-10232","product":"apple_us_10","qty":2}. Telegram Premium — только qty: 1 (одна подписка на заказ), иначе сразу 422 recipient_invalid. Продукт можно указать кодом (product) или идентификатором (product_id).

В момент ответа price_usdt уже зарезервирована на вашем балансе. При неудаче резерв снимается автоматически.

4. Статус и код

GET/v1/orders/{id}
held→sent→pending→successилиfailed
СтатусЗначениеДеньги
heldпринят, сумма зарезервирована, ждёт воркер (секунды)резерв
sent / pendingпередан поставщику, ждём подтверждение (обычно до минуты, опрос каждые 10 с)резерв
successвыполнен; для кодов в карточке появляется поле code (ключу с правом orders; в списке заказов кодов нет, там "code_hidden": "open_order_card")списано
failedне выполнен, причина в errorрезерв возвращён
refundedвозврат после success по решению менеджеравозвращено
{"id":"7c0e…","status":"success","product":"apple_us_10","qty":2,"price_usdt":"19.98",
 "code":"SBX-3F9A-11C2\nSBX-8D04-7B1E","delivered_at":"2026-10-17T12:01:09+00:00", …}
При qty больше 1 в поле code столько кодов, сколько штук, через перевод строки (\n): разбейте строку и выдайте покупателю каждый. Код гифт-карты есть только в карточке заказа и в вебхуке. В списке GET /v1/orders коды не отдаются, чтобы они не попадали в логи и выгрузки.
GET/v1/orders?status=&created_from=&created_to=&limit=50&before=

Список от новых к старым, до 200 за раз. Даты — ISO 8601 с часовым поясом (2026-10-01T00:00:00+03:00), created_to не включается. Если в ответе next_before не null, передайте его в before — получите следующую страницу: {"items":[…],"next_before":"7c0e…"}.

Коды ошибок в error.code: recipient_invalid (логин не найден, деньги возвращены), supplier_unavailable (все поставщики недоступны после переключений и повторов), supplier_rejected, supplier_timeout (поставщик не подтвердил за 10 минут; заказ остаётся в pending, резерв не снимаем, разбирает менеджер), premium_active (у получателя уже есть Telegram Premium — деньги возвращены).

Проверка получателя до оплаты. POST /v1/recipients/check с телом {"product":"stars_100","recipient":{"username":"durov"}} ничего не покупает и не резервирует. Ответ status: ok (найден, в name — имя в Telegram), not_found, premium_active (Premium уже есть — подарить нельзя), invalid (неверный формат), format_ok (товар заранее не проверяется), unknown (проверка не успела — заказ можно создавать, получатель проверится при покупке). Telegram отвечает до 5 секунд — спрашивайте до счёта покупателю.

Подтверждение выдачи. У выполненного заказа Telegram Stars или Premium через собственный канал Platina в карточке и вебхуке есть поле delivery: {"type":"ton_tx","tx_hash":"…","url":"https://tonviewer.com/transaction/…","recipient_name":"…"} — ссылку можно отправить покупателю как подтверждение.

5. Вебхуки

PUT/v1/webhooks
{"url":"https://shop.example/platina/hook"}
→ {"url":"https://shop.example/platina/hook","secret":"whsec_…","events":["order.updated","balance.low","payment.updated","shop_order.updated","catalog.updated","webhook.test"]}

Секрет показывается один раз. Потом POST /v1/webhooks/test отправит пробное событие и вернёт ответ вашего сервера.

Удалить адрес песочницы: DELETE /v1/webhooks тестовым ключом — адрес и секрет стираются, события больше не создаются. Боевой адрес удаляют, выключают и включают в кабинете. Выключенный адрес копит события и отдаёт их после включения, старые первыми; адрес, который 5 суток подряд не отвечает 2xx, приостанавливаем сами — за сутки предупредим уведомлением.

Адрес через API — только для песочницы. PUT /v1/webhooks тестовым ключом pk_test_ задаёт адрес песочницы. Боевой адрес и боевой секрет меняются только в кабинете, в «Вебхуках»: с двухфакторной защитой и кодом подтверждения, как боевой ключ. Боевой ключ получит 403 webhook_live_cabinet_only; читать настройки (GET /v1/webhooks) и слать пробное он может. Адрес — только https://, без логина и пароля в нём.

Смена секрета без простоя. В кабинете — «Выпустить новый секрет» с выбором, сколько ещё действует прежний: сутки, 7 дней или гаснет сразу. В песочнице то же делает {"url":"…","rotate_secret":true}, прежний действует сутки. Всё это время в заголовке две подписи, t=…,v1=<новым>,v1=<прежним>, и previous_secret_until в ответе говорит, до какого момента. Проверяйте подпись так, чтобы подходила любая из v1 — как в примерах ниже. Пока прежний действует, следующий секрет не выпускается (409 webhook_rotation_in_progress): погасите прежний в кабинете, когда сервер уже проверяет новым.

Что приходит

POST /platina/hook
X-Platina-Event: order.updated
User-Agent: Platina-Webhooks/1.0
X-Platina-Delivery: 2a9f…
X-Platina-Attempt: 1
X-Platina-Mode: live
X-Platina-Signature: t=1760702469,v1=8f3c…

{"id":"2a9f…","event":"order.updated","api_version":"2026-10-29","created_at":"2026-10-17T12:01:09+00:00","livemode":true,
 "data":{"id":"7c0e…","client_order_id":"shop-10232","status":"success","code":"SBX-3F9A-11C2", …}}

order.updated приходит на переходах в pending, success и failed; data это та же карточка, что в GET /v1/orders/{id}. balance.low приходит не чаще раза в сутки, когда боевой баланс опускается ниже порога (по умолчанию 50 USDT): {"currency":"USDT","balance_usdt":"41.20","threshold_usdt":"50"}.

Проверка подписи

Подпись это HMAC-SHA256 от строки t + "." + тело с вашим секретом; подписей v1 может быть несколько (после смены секрета), подходит любая. Отбрасывайте события старше 5 минут. Отвечайте 2xx за пару секунд; обработку делайте после ответа.

# Python
import hmac, hashlib, time
def verify(secret: str, body: bytes, header: str) -> bool:
    pairs = [p.split("=", 1) for p in header.split(",") if "=" in p]
    t = next((v for k, v in pairs if k == "t"), None)
    if t is None or abs(time.time() - int(t)) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return any(k == "v1" and hmac.compare_digest(mac, v) for k, v in pairs)
// Node.js
const crypto = require("crypto");
function verify(secret, rawBody, header) {
  const pairs = header.split(",").map(s => s.split("="));
  const t = (pairs.find(([k]) => k === "t") || [])[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const mac = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
  return pairs.some(([k, v]) => k === "v1" && v && v.length === mac.length && crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v)));
}
// PHP
function platina_verify(string $secret, string $rawBody, string $header): bool {
    $t = null; $sigs = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = array_pad(explode('=', $part, 2), 2, '');
        if ($k === 't') $t = $v; elseif ($k === 'v1') $sigs[] = $v;
    }
    if ($t === null || abs(time() - (int)$t) > 300) return false;
    $mac = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($sigs as $v) if (hash_equals($mac, $v)) return true;
    return false;
}

Сырое тело. Подпись считается от байтов тела как они пришли: в Express — express.raw({type: "application/json"}), во Flask — request.get_data(), в FastAPI — await request.body(), в PHP — file_get_contents('php://input'). Пересобранный JSON отличается пробелами и порядком полей, и подпись не сойдётся.

Тестовый вектор (проверку возраста на время теста отключите): секрет whsec_test_vector_platina, заголовок t=1760702469,v1=8d457a89a3936ccd6b9534e7c7d0ccea05abfe98c0aa1909529ccf63b4f7a4d4, тело

{"id":"00000000-0000-4000-8000-000000000000","event":"webhook.test","api_version":"2026-10-29","created_at":"2026-10-17T12:01:09+00:00","livemode":false,"data":{"partner":"demo-shop","source":"cabinet","sandbox":true,"updated_at":"2026-10-17T12:01:09+00:00"}}

Заголовки: X-Platina-Event — событие, X-Platina-Delivery — номер доставки (одинаков у всех повторов; то же, что поле id в теле — дубли отбрасывайте по id, оно под подписью), X-Platina-Attempt — номер попытки (1, 2, …), User-Agent: Platina-Webhooks/1.0, X-Platina-Mode — sandbox или live (то же, что поле livemode в теле), X-Platina-Signature — подпись. Перенаправления 3xx мы не выполняем — укажите конечный адрес. Порядок событий не гарантирован: повтор старого события может прийти после нового, поэтому сверяйте статус заказа, а не порядок прихода.

Если ваш сервер ответил не 2xx или не ответил за 10 с, мы повторим через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч и ещё пять раз через 12 ч — около 3 суток, затем остановимся (в песочнице — через 1, 5 и 30 мин). К паузам добавляем разброс ±10 %; ответ 429 или 503 с Retry-After в секундах удлиняет паузу до 12 ч; ответ 410 Gone выключает адрес — события копятся, пока вы его не включите в кабинете или пока не дойдёт пробное (тогда включим сами). Номер попытки — в заголовке X-Platina-Attempt, версия формата — поле api_version в теле, у каждого объекта в data есть updated_at. Журнал доставок хранится 90 дней. Повторы идут на адрес, сохранённый на момент попытки: поменяли адрес — недоставленное придёт уже туда. Последние 20 доставок — в GET /v1/webhooks, весь журнал страницами — GET /v1/webhooks/deliveries?status=failed&limit=50 (следующая страница — before=next_before), каждая попытка с ответом вашего сервера — в кабинете («Вебхуки» → «попытки и данные»). Событие может прийти дважды: отбрасывайте дубли по полю id в теле — оно под подписью и совпадает с X-Platina-Delivery.

6. Пополнение баланса

POST/v1/topups
{"method":"usdt","amount_usdt":"500","client_ref":"topup-2026-10-28-1"}      // usdt — в USDT 1:1; sbp и invoice — amount_rub
{"method":"invoice","amount_rub":"50000"}                                         // рубли зачислятся в USDT по курсу на момент зачисления
→ {"id":"c41a…","status":"pending","currency":"USDT","amount_rub":"50000.00","amount_usdt":null,
   "requisites":{"company":"…","inn":"…","account":"…","payment_code":"PLT-C41A9F02","comment":"Пополнение Platina PLT-C41A9F02"}}

Укажите payment_code в назначении платежа. После поступления денег менеджер подтверждает заявку, баланс растёт одной записью в леджере, вы видите это в GET /v1/balance и GET /v1/ledger. Рублёвая заявка конвертируется в USDT по курсу Rapira с запасом 0,5 % на момент зачисления (курс — в note записи леджера, сумма — в amount_usdt заявки). Для USDT в реквизитах приходит свой адрес TRC-20 под эту заявку: он действует 60 минут, перевод зачисляется 1:1 автоматически, без менеджера.

client_ref защищает от дублей: повтор запроса с тем же значением вернёт ту же заявку (200), а не создаст вторую; тот же client_ref с другой суммой или способом — 409 idempotency_conflict. Пока кабинет на паузе, боевые заявки не принимаются (403).

В песочнице заявку подтверждаете сами: POST /v1/topups/{id}/confirm. Минимальная сумма — 10 USDT или 1 000 ₽.

GET/v1/balance
{"sandbox":false,"currency":"USDT","balance_usdt":"566.91","credit_limit_usdt":"0.00","available_usdt":"566.91","reserve_usdt":"0.00","reserve_orders":0}
GET/v1/ledger?limit=50&before_id=

Все движения (amount_usdt со знаком): topup, hold (резерв под заказ), release (возврат резерва), refund, payin и fee (оплата покупателя и комиссия, раздел 6b). Баланс всегда равен сумме записей, записи не редактируются.

6a. Виртуальные карты

Карты Visa USD/EUR для оплаты зарубежных сервисов. Выпуск сразу с начальным балансом (initial_amount, целые единицы валюты карты) — так дешевле, чем выпустить пустую и пополнить. Цены в USDT с учётом тарифа — в GET /v1/cards/products. В песочнице карты мок-режима: те же ответы, тестовые реквизиты, деньги у поставщика не тратятся.

POST/v1/cards
{"client_card_id":"buyer-42-1","product":"product_01m3s0p1fcfm99mp6qy3drn5zj","initial_amount":5,"label":"buyer 42"}
→ 201 {"operation":{"id":"…","kind":"issue","status":"success","price_usdt":"9.38"},
       "card":{"id":"7","status":"active","currency":"usd","masked_number":"424242******6261","exp_month":6,"exp_year":2029}}

Повтор с тем же client_card_id возвращает ту же карту (200). Статус pending в операции — поставщик ещё выпускает: опрашивайте GET /v1/cards/operations/{id}.

GET/v1/cards/{id}/credentials
{"pan":"4242…","cvv":"…","exp_month":6,"exp_year":2029,"pin_code":null,"three_ds_password":null}

Platina реквизиты не хранит: ответ приходит от эмитента, показывайте его покупателю и не кэшируйте (Cache-Control: no-store). Коды подтверждения онлайн-оплаты — GET /v1/cards/{id}/3ds-codes, опрашивайте во время оплаты, код живёт до expires_at.

POST/v1/cards/{id}/topup
{"client_ref":"buyer-42-top-1","units":10}      // сумма в валюте карты, карта должна быть active

Блокировка POST /v1/cards/{id}/block {"confirm":true} — навсегда, остаток на карте не возвращается. Баланс и операции: GET /v1/cards/{id}/balance, GET /v1/cards/{id}/transactions. Списание с вашего баланса — одной записью hold на операцию; при отказе поставщика резерв снимается (release).

6b. Приём платежей (доп. модуль)

Принимайте оплату от своих покупателей через Platina: QR СБП, USDT TRC20, криптовалютой в нескольких сетях или наличными в кассе. Покупатель платит в рублях, вам на баланс зачисляется USDT за вычетом комиссии метода (credited_usdt и курс credit_rate в карточке после оплаты), с этого же баланса оплачиваются заказы. Нужен ключ со scope payments. В песочнице модуль доступен всем, в бою его подключает менеджер; что доступно прямо сейчас, показывает GET /v1/payments/methods.

POST/v1/payments
{"client_payment_id":"order-981","method":"sbp_qr","amount_rub":"1490","customer_ref":"tg:5512"}   // sbp_qr | usdt_trc20 | crypto | cash
→ 201 {"id":"…","status":"pending","amount_rub":"1490.00","fee_pct":"3.000","expires_at":"…",
       "instructions":{"qr_payload":"https://qr.nspk.ru/…"}}

Что показать покупателю, лежит в instructions:

Повтор с тем же client_payment_id возвращает тот же платёж (200), с другой суммой, методом или активом — 409. Статусы: pending → paid | expired | canceled | failed; review — деньги пришли после срока или отмены, не полностью или с высоким AML-риском, решает менеджер. expired у crypto и usdt_trc20 не окончательный: перевод в течение 120 минут после срока зачисляется, и платёж переходит в paid — не выдавайте товар повторно и не считайте такой счёт потерянным. На каждую смену статуса приходит вебхук payment.updated с карточкой платежа и вашим customer_ref; зачислено credited_rub = paid_rub − fee_rub.

Если вы продаёте через витрину Platina Apps, на каждую смену состояния покупки приходит shop_order.updated: номер покупки, который видит покупатель (number), статус, оплачено и комиссия (paid_rub, fee_rub), связь с заказом (order_id, client_order_id = shop:<id>) и отметка возврата (refund_rub, refunded_at). Кода товара в этом событии нет — он приходит в order.updated.

catalog.updated — изменился каталог: закупка товара сдвинулась на 0,5 % и больше, товар появился, пропал у поставщиков или вернулся. Проверка — раз в 5 минут, в одном событии до 500 изменений (count, changes). У каждого изменения: код и название товара (product, name), что случилось (change: new, price, available, unavailable), ваша цена до и после (old_price_rub, price_rub, по вашему тарифу и курсу на момент события) и за что она (per: item — за штуку, usd — за $1 пополнения). Событие по умолчанию выключено — включите его в кабинете, раздел «Вебхуки»: так интеграция, которая не знает нового события, не получит его неожиданно.

Отменить ожидающий платёж: POST /v1/payments/{id}/cancel. В песочнице «оплачиваете» сами: POST /v1/payments/{id}/confirm (можно с {"amount_rub":"…"}, чтобы проверить недоплату). В леджере оплата видна двумя записями: payin (+сумма) и fee (−комиссия).

7. Ошибки

Любая ошибка приходит в одном формате: {"code":"…","message":"…","retryable":true|false,"request_id":"req_…"}. Сообщения на русском и для человека, код для программы. У ошибки проверки (422 validation_error) есть ещё fields: [{"field":"qty","message":"слишком большое значение","type":"less_than_equal"}].

request_id есть в заголовке X-Request-Id каждого ответа (можно прислать свой, 8–64 символа: буквы, цифры, ._:-). Пишите его в свои логи: по нему запрос находится в кабинете на странице «Журнал API» (там все запросы вашего ключа за 14 дней, с ответом и временем) и у поддержки.

Лимит: 10 запросов в секунду на ключ, всплеск до 20. В каждом ответе: X-RateLimit-Limit — ёмкость (20), X-RateLimit-Remaining — сколько запросов можно сделать прямо сейчас, X-RateLimit-Reset — через сколько секунд снова доступны все 20, X-RateLimit-Policy: 10;w=1;burst=20. При 429 — Retry-After в секундах.

HTTPcodeКогдаЧто делать
400bad_requestзапрос не подходит этому методупрочитать message и исправить запрос
400confirm_requiredнеобратимое действие без confirm=true (например, блокировка карты)повторить с confirm=true, если действие нужно
400webhook_https_requiredадрес вебхука не httpsуказать адрес https://
400webhook_url_blockedадрес вебхука ведёт во внутреннюю сеть или не резолвитсяуказать публичный адрес
401unauthorizedнет ключа, ключ неверный, отозван или истёкпроверить заголовок Authorization и ключ
402insufficient_fundsне хватает денег на балансе под резерв заказапополнить баланс
403forbiddenу ключа нет нужного права, запрос пришёл не с разрешённого IP или кабинет на паузев message сказано, что именно; права и IP ключа — в «Ключах API»
403blockedполучатель заблокирован антифродомпроверить получателя; если ошибка — написать в поддержку
403spend_limitзаказ упёрся в ваш лимит расходов: на день, на один заказ или на ключ API в день (поле limit — какой именно)дождаться следующих суток по Москве или поднять лимит (владелец или администратор кабинета)
403payments_disabledприём оплат не подключёнподключить модуль оплат у менеджера
403webhook_live_cabinet_onlyбоевой ключ меняет или удаляет адрес вебхукабоевой адрес задать в кабинете
404not_foundнет такого метода или объектапроверить адрес и id
405method_not_allowedадрес не принимает этот HTTP-методсмотреть заголовок Allow
409idempotency_conflictтот же client_order_id с другим составом заказановый client_order_id или прежний состав
409product_unavailableтовара нет в продаже или нет поставщикаобновить каталог
409card_not_activeкарта заблокирована или ещё не выпущенапроверить статус карты
409conflictплатёж уже в другом состоянииперечитать платёж
409webhook_not_configuredпробное событие без адреса вебхукасначала задать адрес
409webhook_rotation_in_progressновый секрет, пока прежний ещё действуетдождаться конца окна или погасить прежний
422validation_errorполе запроса не прошло проверку — какое, сказано в fieldsисправить запрос
422recipient_invalidполучатель или сумма не проходят проверкуисправить данные получателя или сумму
422premium_activeу получателя уже активен Telegram Premium — подарить подписку нельзяпредложить покупателю Stars или подарить Premium после окончания его подписки
422invalid_cursorкурсор before не из этого спискапередать next_before из прошлого ответа
422topup_limitуже несколько заявок в USDT ждут оплатыоплатить или отменить одну из них, затем создать новую
422topup_invalidзаявку на пополнение нельзя создать (сумма, способ, курс)прочитать message и поправить заявку
422payment_invalidплатёж нельзя создать (сумма, способ)прочитать message и поправить платёж
422method_unavailableспособ оплаты сейчас недоступенвыбрать другой способ
429rate_limitedбольше 10 запросов в секунду на ключ (всплеск до 20)подождать Retry-After секунд и повторить
429too_many_bad_keysс вашего IP подряд шли запросы с неверным ключом — временно отвечаем 429 всем ключам с этого адресаисправить ключ в настройках сервера и подождать Retry-After секунд
429limit_exceededпревышен лимит партнёра: сумма заказа или оборот за суткинаписать менеджеру
429customer_limitпревышен лимит покупок на одного получателяповторить позже
500server_errorсбой на стороне Platinaповторить; если не проходит — прислать request_id в поддержку
503fx_staleкурс USDT/RUB устарел — заказы временно не принимаютсяповторить через пару минут (retryable)
503payment_provider_unavailableплатёжный провайдер не ответил при создании платежаповторить позже (retryable)
503supplier_unavailableвсе поставщики товара недоступныповторить позже (retryable)

8. Правила песочницы

Песочница детерминирована, сценарий выбирается префиксом получателя. Для товаров без получателя (коды) заказ всегда успешен. Какие сценарии вы уже прошли — на странице кабинета «Готовность к бою».

ПолучательРезультат
любой, например gabensuccess сразу; код вида SBX-XXXX-XXXX для гифт-карт
fail…отказ сразу: ответ 422 recipient_invalid, заказ не создаётся, деньги не резервируются
failasync…заказ принят (201, pending), затем failed с error.code = recipient_invalid, вебхук order.updated, резерв возвращён — так отказывает настоящий поставщик
down…поставщик недоступен: переключение на второго поставщика, потом повторы, в итоге failed с supplier_unavailable
slow…pending несколько секунд (до следующего опроса поставщика), затем success; вебхук на каждом переходе

Цены в песочнице близки к боевым, но не равны им. Баланс песочницы не связан с боевым.

9. Переход в бой

Когда тестовые заказы проходят до success, а вебхук проверяет подпись, сверьтесь с 14 проверками на странице «Готовность к бою» в кабинете. Боевой API открыт сразу, запрашивать его у менеджера не нужно: боевой ключ pk_live_… выпускает любой сотрудник с правом на ключи (владелец, администратор, разработчик), у которого включена двухфакторная защита. Владельцу 2FA нужна в любом случае: это одна из 14 проверок. Для него адрес вебхука обязан быть https. Остальное не меняется: те же эндпоинты, те же коды ошибок. Мы рекомендуем ограничить боевой ключ списком IP и поднять balance.low до двух дневных оборотов.

Платформа работает на законных основаниях: заказы исполняются только на аккаунты и адреса, указанные покупателем, покупатель проходит ваши проверки, а подозрительные логины и покупатели попадают в общий блок-лист сети.

Справочник эндпоинтов

МетодПутьScopeНазначение
GET/v1/meta—версия API, без ключа
GET/v1/mereadпроверка ключа
GET/v1/catalogreadпродукты и цены; фильтры category, region
GET/v1/products/{id}readодин продукт
GET/v1/ratesreadкурс USDT/RUB
POST/v1/ordersordersсоздать заказ (идемпотентно)
GET/v1/orders/{id}readкарточка заказа; код товара — только ключу с orders
GET/v1/ordersreadсписок; фильтры status, client_order_id
POST/v1/recipients/checkreadпроверка получателя до оплаты: найден, нет такого, Premium уже есть
PUT/v1/webhooksordersадрес и секрет
GET/v1/webhooksreadнастройки и 20 последних доставок
DELETE/v1/webhooksordersудалить адрес песочницы
GET/v1/webhooks/deliveriesreadжурнал доставок; фильтры status, event
POST/v1/webhooks/testordersпробное событие
POST/v1/topupstopupsзаявка на пополнение
GET/v1/topups, /v1/topups/{id}readзаявки
POST/v1/topups/{id}/confirmtopupsтолько песочница
GET/v1/cards/productsreadтипы карт и цены выпуска/пополнения
POST/v1/cardsordersвыпуск карты с начальным балансом (идемпотентно)
GET/v1/cards, /v1/cards/{id}readкарты; ?refresh=true перечитать статус
GET/v1/cards/{id}/credentialsordersPAN, CVV, срок (не хранятся)
GET/v1/cards/{id}/balance, …/transactionsreadбаланс, операции
GET/v1/cards/{id}/3ds-codesordersкоды 3-DS
POST/v1/cards/{id}/topupordersпополнение (идемпотентно)
POST/v1/cards/{id}/blockordersблокировка навсегда
GET/v1/cards/operations/{id}readоперация выпуска/пополнения
GET/v1/payments/methodsreadметоды приёма, комиссии, сроки
POST/v1/paymentspaymentsвыставить платёж покупателю (идемпотентно)
GET/v1/payments, /v1/payments/{id}readплатежи; фильтры status, client_payment_id, customer_ref
POST/v1/payments/{id}/cancelpaymentsотменить ожидающий
POST/v1/payments/{id}/confirmpaymentsтолько песочница
GET/v1/balancereadбаланс и доступно
GET/v1/ledgerreadдвижения по счёту

Полная схема запросов и ответов: /docs (Swagger) и /openapi.json.