CChainPay
Увійти

Payout API

API ChainPay для виведення криптовалюти на адресу платника (withdraw). На відміну від депозитів (публічно, без ключа — див. розділ Deposits), payout рухає гроші з нашого боку, тому весь /payout/* — за спільним x-api-key, а кожен конкретний гаманець додатково захищений своїм walletAccessToken.

Статус: скоро. Виводи ще не запущені. Описаний нижче API — запланований контракт, він може змінитись до запуску.

Базовий URL

https://api.chainpay.site

TLS обов'язковий; HTTP-запити не приймаються.

Підтримувані мережі

tron, bsc, eth, polygon, arbitrum. solana не підтримується.


Модель — виділений гаманець на клієнта, без внутрішнього обліку

Кожен payout-гаманець — це реальна HD-адреса в мережі (та сама механіка деривації, що й для deposit-адрес, тільки з purpose=payout). Ми ніколи не ведемо внутрішній баланс клієнта — перед кожним виводом читається реальний баланс адреси on-chain. Клієнт сам поповнює цю адресу зі свого боку (це не "зняти з нашого рахунку", а "вивести те, що там реально лежить").

Одержувач завжди отримує повну запрошену суму amount — жодна комісія з токена не утримується. Єдина реальна вартість виводу — мережева (Energy/ Bandwidth на TRON, gas на EVM-чейнах) — і вона оплачується повністю окремо, з власного газового гаманця клієнта (розділ 1 нижче), ніяк не торкаючись суми, яку отримає destination.


1. POST /payout/wallet — отримати/створити гаманець

Потрібен заголовок x-api-key. Get-or-create: однакова комбінація (chain, tokenSymbol, externalReference) завжди повертає ту саму адресу.

curl -X POST https://api.chainpay.site/payout/wallet \
  -H 'content-type: application/json' \
  -H 'x-api-key: YOUR_API_KEY' \
  -d '{
    "chain": "tron",
    "tokenSymbol": "USDT",
    "externalReference": "site-42",
    "callbackUrl": "https://your-service.example.com/payout-webhook?token=your-secret"
  }'

Тіло запиту

ПолеОбов'язковеОпис
chainтакtron, bsc, eth, polygon або arbitrum
tokenSymbolтакТокен, яким виводитимете з цього гаманця (напр. USDT)
externalReferenceтакВаш власний ідентифікатор (сайт/клієнт/акаунт)
callbackUrlтакПублічний HTTPS/HTTP URL для вебхуків про статус виводу. Не може вказувати на localhost/приватну IP.

Відповідь (201) — перше створення

{
  "id": "6311badc-787a-4e33-804a-9562a553e320",
  "chain": "tron",
  "tokenSymbol": "USDT",
  "address": "THjTgKDGj3Biar7ZTSmYZAWsqkTKhWGv5Y",
  "externalReference": "site-42",
  "walletAccessToken": "9f2c8a1e4b3d7f60...(64 hex-символи)",
  "gasStationAddress": "TDZkhJWMeaFAhefQXK4mG8GtkSZahw13jK"
}

walletAccessToken показується рівно один раз, тут і зараз. Ніде більше його побачити не можна — тільки хеш зберігається в базі. Зберігайте його одразу на своєму боці; він знадобиться для кожного POST /payout/withdraw з цього гаманця.

gasStationAddress — ваш власний гаманець для оплати мережевих ресурсів цього чейну (Energy/Bandwidth на TRON, gas на EVM). Це не адреса для виводу коштів — на неї потрібно надіслати трохи нативної валюти чейну (TRX для tron, ETH/BNB/MATIC для відповідного EVM-чейну — не токен, яким виводите), щоб система могла профінансувати кожен ваш вивід. Один такий гаманець на весь ваш externalReference на цьому чейні — спільний для всіх ваших payout-гаманців незалежно від токена (USDT, USDC тощо), заводити окремий на кожен токен не потрібно. Якщо на ньому не вистачає — /payout/withdraw просто провалиться (failed), без автоматичного покриття з нашого боку; поповніть і повторіть спробу з новим invoiceId. Перевірити поточний баланс можна через POST /payout/estimate (нижче).

Відповідь при повторному виклику (гаманець уже існує)

Та сама структура, але без поля walletAccessToken (gasStationAddress повертається завжди, навіть при повторному виклику):

{
  "id": "6311badc-787a-4e33-804a-9562a553e320",
  "chain": "tron",
  "tokenSymbol": "USDT",
  "address": "THjTgKDGj3Biar7ZTSmYZAWsqkTKhWGv5Y",
  "externalReference": "site-42",
  "gasStationAddress": "TDZkhJWMeaFAhefQXK4mG8GtkSZahw13jK"
}

Помилки (400)

  • chain, tokenSymbol, externalReference and callbackUrl are required.
  • Invalid callbackUrl: ... — не HTTPS/HTTP, або вказує на приватну/локальну адресу.
  • Payout not yet supported for chain: <chain> — чейн не входить у список вище.
  • No active payout SigningKey configured for chain: ... / No active payout_gas_station SigningKey configured for chain: ... — payout ще не введено в експлуатацію на цьому чейні (див. "Статус" на початку документа).

2. POST /payout/estimate — попередній розрахунок (без запису)

Перевірити, чи вистачає балансу гаманця й газової станції до реального виводу — не створює жодного запису, можна викликати скільки завгодно разів.

curl -X POST https://api.chainpay.site/payout/estimate \
  -H 'content-type: application/json' \
  -H 'x-api-key: YOUR_API_KEY' \
  -d '{
    "address": "THjTgKDGj3Biar7ZTSmYZAWsqkTKhWGv5Y",
    "destination": "TDestinationAddressOfThePayer...",
    "amount": "100.5",
    "walletAccessToken": "9f2c8a1e4b3d7f60..."
  }'

Ті самі поля, що й у /payout/withdraw, тільки без invoiceId.

Відповідь (200)

{
  "balance": "250.000000000000000000",
  "sufficientBalance": true,
  "gasStationAddress": "TDZkhJWMeaFAhefQXK4mG8GtkSZahw13jK",
  "gasStationBalance": "12.5",
  "sufficientGas": true
}
  • balance — реальний баланс гаманця. Для TRON — з нашого власного безперервного стеження за мережею (оновлюється в реальному часі, без затримки на окремий запит); для інших мереж — живе on-chain-читання, яке цей виклик заодно кешує на кілька хвилин, щоб наступний /payout/withdraw міг одразу відмовити (400), якщо вже видно, що не вистачить.
  • sufficientBalance — false, якщо amount більше за поточний баланс. Це перевірка лише токена — навіть якщо тут true, вивід все одно може провалитись через нестачу газу на gas-станції; перевіряйте sufficientGas окремо.
  • gasStationAddress/gasStationBalance — ваш газовий гаманець і скільки на ньому зараз нативної валюти чейну (TRX для tron, ETH/BNB/MATIC для відповідного EVM-чейну).
  • sufficientGas — чи вистачає gasStationBalance на оцінену мережеву вартість саме цього переказу. Поле відсутнє (не false), якщо оцінку не вдалось порахувати — тоді покладайтесь на запас на газовій станції.
  • Помилки — ті самі, що в /payout/wallet//payout/withdraw (невірний walletAccessToken, гаманець не знайдено, невалідна destination тощо).

3. POST /payout/withdraw — запит на вивід

Потрібен x-api-key і walletAccessToken цього конкретного гаманця. Повертається одразу — мережевого підтвердження запит не чекає, сам вивід виконується асинхронно (фоновий воркер, кожні 15 с).

Якщо нещодавно (останні кілька хвилин) відомий баланс гаманця/газової станції — з попереднього /payout/estimate або попереднього виводу — явно не покриває суму чи мережеву вартість, запит одразу відхиляється (400), не чекаючи фонового воркера. Це рання, best-effort перевірка за вже відомими даними, не жива звірка з блокчейном — якщо щось щойно змінилось чи ще ніколи не перевірялось, запит все одно прийметься, а реальна (авторитетна) перевірка відбудеться пізніше, безпосередньо перед відправкою.

curl -X POST https://api.chainpay.site/payout/withdraw \
  -H 'content-type: application/json' \
  -H 'x-api-key: YOUR_API_KEY' \
  -d '{
    "address": "THjTgKDGj3Biar7ZTSmYZAWsqkTKhWGv5Y",
    "destination": "TDestinationAddressOfThePayer...",
    "amount": "100.5",
    "invoiceId": "withdraw-2026-09-14-001",
    "walletAccessToken": "9f2c8a1e4b3d7f60..."
  }'

Тіло запиту

ПолеОбов'язковеОпис
addressтакАдреса payout-гаманця (та, що повернув /payout/wallet)
destinationтакКуди вивести кошти — адреса платника, перевіряється формат для цієї мережі
amountтакСума до виводу, у токені гаманця — саме стільки й піде на destination, без утримань
invoiceIdтакКлюч ідемпотентності. Повторний виклик з тим самим invoiceId поверне той самий запис, не задвоїть вивід
walletAccessTokenтакТокен саме цього гаманця (виданий один раз при створенні)

Відповідь (201)

{
  "id": "b7e1...",
  "status": "pending",
  "amount": "100.500000000000000000",
  "invoiceId": "withdraw-2026-09-14-001"
}

amount — те, що ви запросили, і те, що реально піде на destination — тут немає окремого "нетто"-поля, комісія ніде не утримується.

Помилки (400 / 401 / 404)

  • address, destination, amount, invoiceId and walletAccessToken are required.
  • No payout wallet registered for address: ... (404) — така адреса не заводилась через /payout/wallet.
  • Invalid walletAccessToken — токен не збігається (timing-safe порівняння).
  • destination is not a valid <chain> address: ....
  • amount must be a positive number.
  • Insufficient balance: wallet ... had ... as of ..., needs ... — рання перевірка за нещодавно відомим балансом (див. вище), не гарантія: якщо балансу дійсно не вистачить, а кеш застарів чи його ще нема, дізнаєтесь про це пізніше через failed-вебхук, а не з цієї помилки одразу.
  • Insufficient <native currency> on gas station ...: has ..., needs ~... — та сама рання, best-effort перевірка, для нативної валюти на вашій gas-станції: якщо оцінку мережевої вартості не вдалось порахувати або дані ще не встигли оновитись, ця помилка не спрацює одразу, і про реальну нестачу газу дізнаєтесь пізніше через failed-вебхук.

4. GET /payout/:id/status — статус запиту

Надійний fallback, якщо вебхук не дійшов.

{
  "id": "b7e1...",
  "status": "confirmed",
  "amount": "100.500000000000000000",
  "destination": "TDestinationAddressOfThePayer...",
  "txHash": "802999e236cf8f...",
  "failureReason": null,
  "createdAt": "2026-09-14T06:52:48.000Z",
  "confirmedAt": "2026-09-14T06:54:49.000Z"
}

status:

  • pending — прийнято, ще не оброблено фоновим воркером або обробка триває;
  • confirmed — кошти реально відправлені й підтверджені в мережі, txHash заповнено;
  • failed — вивід не відбувся, деталі в failureReason (напр. Insufficient balance: ...).

5. Вебхуки на callbackUrl

Best-effort, одна спроба, без ретраїв (на відміну від депозитних вебхуків) — GET /payout/:id/status вище є гарантованим fallback. Так само, як і депозитні вебхуки, не підписуються HMAC — верифікуйте секретним токеном у query-рядку callbackUrl і/або по IP відправника (157.180.64.58, той самий сервер, що й для депозитів).

{
  "externalReference": "site-42",
  "invoiceId": "withdraw-2026-09-14-001",
  "status": "confirmed",
  "txHash": "802999e236cf8f..."
}

Надсилається і на confirmed, і на failed (тоді txHash: null, причину дивіться через GET /payout/:id/status).


6. Захист

Двошаровий, обидва рівні обов'язкові:

  1. x-api-key — спільний секрет на весь /payout/*, той самий для всіх клієнтів (немає реєстрації/акаунтів — видається вам вручну, приватним каналом, при підключенні). Невдале порівняння → 404, не 401 (не підказуємо, що ендпоінт існує).
  2. walletAccessToken — персональний bearer-токен саме цього гаманця. 256 біт випадковості, показується рівно один раз у відповіді на POST /payout/wallet. Звіряється timing-safe-порівнянням (crypto.timingSafeEqual) — час перевірки не залежить від того, наскільки токен "схожий" на правильний, що унеможливлює підбір по затримці відповіді. Загублений токен відновлюється тільки вручну через адмінку (POST /admin/payout-wallets/:id/regenerate-token, без самообслуговування).

Плюс: SSRF-захист на callbackUrl (як і в депозитів), і додаткова внутрішня перевірка перед кожним реальним виводом коштів.


7. Типовий інтеграційний цикл

  1. Одноразово, при підключенні клієнта/сайту — POST /payout/wallet з унікальним externalReference → зберігаєте address, walletAccessToken і gasStationAddress у себе.
  2. Поповнюєте address тими коштами (токеном), які плануєте виводити гравцям/користувачам, і gasStationAddress — нативною валютою чейну для мережевих витрат.
  3. (Опційно) Перед виводом — POST /payout/estimate, щоб переконатись, що балансу токена й газу вистачає.
  4. Коли користувач хоче вивід — POST /payout/withdraw з унікальним invoiceId → одразу отримуєте pending.
  5. Отримуєте confirmed/failed-вебхук на callbackUrl.
  6. Якщо вебхук не прийшов за розумний час → фолбек на GET /payout/:id/status.