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. Захист
Двошаровий, обидва рівні обов'язкові:
x-api-key— спільний секрет на весь/payout/*, той самий для всіх клієнтів (немає реєстрації/акаунтів — видається вам вручну, приватним каналом, при підключенні). Невдале порівняння →404, не401(не підказуємо, що ендпоінт існує).walletAccessToken— персональний bearer-токен саме цього гаманця. 256 біт випадковості, показується рівно один раз у відповіді наPOST /payout/wallet. Звіряється timing-safe-порівнянням (crypto.timingSafeEqual) — час перевірки не залежить від того, наскільки токен "схожий" на правильний, що унеможливлює підбір по затримці відповіді. Загублений токен відновлюється тільки вручну через адмінку (POST /admin/payout-wallets/:id/regenerate-token, без самообслуговування).
Плюс: SSRF-захист на callbackUrl (як і в депозитів), і додаткова
внутрішня перевірка перед кожним реальним виводом коштів.
7. Типовий інтеграційний цикл
- Одноразово, при підключенні клієнта/сайту —
POST /payout/walletз унікальнимexternalReference→ зберігаєтеaddress,walletAccessTokenіgasStationAddressу себе. - Поповнюєте
addressтими коштами (токеном), які плануєте виводити гравцям/користувачам, іgasStationAddress— нативною валютою чейну для мережевих витрат. - (Опційно) Перед виводом —
POST /payout/estimate, щоб переконатись, що балансу токена й газу вистачає. - Коли користувач хоче вивід —
POST /payout/withdrawз унікальнимinvoiceId→ одразу отримуєтеpending. - Отримуєте
confirmed/failed-вебхук наcallbackUrl. - Якщо вебхук не прийшов за розумний час → фолбек на
GET /payout/:id/status.