GoSwap Pay API

Платёжный шлюз для приложений в Telegram: принимайте деньги от своих пользователей и платите им — обе операции проводкой внутри шлюза, без выхода в блокчейн. Выплата пользователю бесплатна и мгновенна.

Base: /apiLive форма Crypto Payкомиссия приёма: 0%

Введение

Используйте этот API на backend вашего приложения. Форма запросов намеренно повторяет Crypto Pay: если интеграция с ним уже написана, миграция — это замена базового URL и токена, а не переписывание кода.

https://goswap.site/api/

Подключение — самообслуживание: кнопка GoPay в боте @goswap создаёт организацию и выдаёт токен. Счета и приём работают сразу; право на выплаты (transfer) включается после разговора с нами.

Модель денег

рекламодатель ──createInvoice──► баланс приложения в GoSwap │ transfer ▼ баланс пользователя в GoSwap │ вывод в цепь ▼ его кошелёк

Деньги, пришедшие от плательщиков, лежат на том же счёте, с которого уходят выплаты. Отдельная ликвидность под выплаты не нужна: оборот сам себя финансирует. Комиссия шлюза — ноль для всех: движение внутри — это проводка в базе, она нам ничего не стоит. Зарабатываем на выводе в цепь и обмене монет.

Три вещи, которые стоит понять до построения учёта:

У организации нет кошелька. «Счёт организации» — строка в наших книгах: сколько мы вам должны. Адрес, который получает ваш клиент для пополнения, нужен ровно для одного — зафиксировать, чей это платёж; деньги с него сметаются в общий кошелёк сервиса. Адрес опознаёт, кошелёк хранит, книги считают.

Кредита нет. Приложение тратит только то, что у него лежит. Попытка отправить больше — отказ, не отрицательный баланс.

Заработок забирается в боте. Кнопкой, владельцем организации. В API такого метода нет намеренно: токен, которым можно увести заработок на чужой личный баланс, — кража одним запросом.

Авторизация

Для запросов нужен заголовок:

Crypto-Pay-API-Token: ваш_токен

Токен выдаётся при создании организации в GoPay и выглядит как gsp_1a2b3c… — по префиксу его удобно искать в логах и коде, если он куда-то попал. Перевыпустить утёкший токен можно кнопкой в боте: старый умирает мгновенно. Только HTTPS: токен в заголовке по обычному HTTP — это тот же токен, отданный любому по дороге.

401 — нет токена или он неизвестен. 403 — у приложения нет права на этот метод (например, transfer до включения).

Формат ответов

Успех:

{"ok": true, "result": { … }}

Ошибка — код HTTP и тело:

{"detail": "человеческое описание"}
КодКогда
401нет токена или он неизвестен
403у приложения нет права на этот метод
400неверные параметры, сумма вне предела или недостаточно средств
422тело запроса не разобрано: нет обязательного поля, строка длиннее предела
429слишком часто — лимит запросов в минуту на приложение (счета 60, чеки и переводы 20) или всплеск с одного IP; заголовок Retry-After говорит, через сколько секунд повторить. Тот же код — исчерпан суточный лимит переводов
503временно недоступно — например, адрес для пополнения ещё готовится; заголовок Retry-After, если он есть, говорит, через сколько секунд повторить. При отказе ни адреса, ни окна не создаётся

Повтор после 429 безопасен: ни счёт, ни перевод при отказе не создаются. Перевод и чек идемпотентны по spend_id — повторный запрос с тем же spend_id вернёт прежний результат и денег не сдвинет.

GETgetMe

Жив ли токен и что им можно. Первый вызов любой интеграции.

{"app_id": 1, "name": "Traffy", "fee_bps": 0, "can_transfer": true, "daily_limit_usd": "500"}

GETgetBalance

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

[{"currency_code": "USDT", "available": "1250.500000"}]

POSTcreateInvoice

Счёт на пополнение. Пользователь платит — деньги падают на баланс приложения.

ПолеТипОбяз.Смысл
assetстрокадаUSDT, TRX, ETH, BNB
amountстрокадасумма, десятичной строкой. Пределы: USDT 100 000, TRX 1 000 000, BNB 200, ETH 50 — выше вернём 400
descriptionстроканетчто видит плательщик, до 1024 символов. Показывается как текст: HTML и скрипты в нём не исполняются нигде
payloadстроканетваш номер заказа, до 4096 символов. Возвращается как прислали
expires_inчислонетсекунд до истечения, по умолчанию 3600, от 60 до 2 678 400
{"invoice_id": 7304918265, "hash": "kO6EXkDdCBh3UoU6uOAxxA", "asset": "USDT", "amount": "50", "status": "active", "bot_invoice_url": "https://t.me/goswap?start=inv_kO6EXk…", "mini_app_invoice_url": "https://t.me/goswap/app?startapp=kO6EXk…", "receipt_url": "https://t.me/goswap?start=rc_kO6EXk…", "expires_at": "2026-08-25T03:47:38+00:00"}

Две ссылки на оплату — какую отправить плательщику, решаете вы.

receipt_url — чек оплаты в боте GoSwap. Сделайте этой ссылкой номер счёта в своём сообщении об оплате: клиент откроет чек и кошелёк GoSwap.

invoice_id — случайное десятизначное число, не порядковый номер: по нему нельзя оценить, сколько счетов выставляют другие приложения. Связывайте оплату со своей записью по payload или hash.

payload заполняйте всегда. Он вернётся в вебхуке — это единственное, чем вы свяжете оплату со своей записью. Без него придётся угадывать по сумме и времени, а две одинаковые оплаты в одну минуту — не редкость.

GETgetInvoices

Счета приложения, новые сверху.

ПараметрТипОбяз.Смысл
invoice_idsстроканетномера счетов через запятую: 7304918265,5512093847. Не больше 500
statusстроканетactive, paid или expired
assetстроканетмонета счёта: USDT, TRX…
countчислонетсколько счетов вернуть, от 1 до 1000 (больше — вернём 1000). По умолчанию 100, с invoice_ids — все найденные
offsetчислонетсколько самых новых счетов пропустить, по умолчанию 0
{"items": [{"invoice_id": 7304918265, "hash": "kO6EXk…", "asset": "USDT", "amount": "49.99", "received": "49.99", "target": "50", "fee_amount": "0", "status": "paid", "payload": "traffy-order-991", "created_at": "…", "paid_at": "…", "receipt_url": "https://t.me/goswap?start=rc_kO6EXk…"}]}

Покупатель нажал «Я оплатил» — спросите счёт по номеру. getInvoices?invoice_ids=7304918265 вернёт ровно этот счёт с текущим статусом. Так проверяют оплату, если вебхук не дошёл или ваш сервер не отвечал в момент оплаты. По номеру находится любой счёт, даже если после него выставлено больше 1000 новых. Сверяйте invoice_id и payload в ответе со своим заказом — не берите items[0] вслепую.

expired — срок вышел, но это не окончательно: перевод, пришедший после срока, может закрыть счёт — он станет paid, и придёт вебхук invoice_paid.

Номер чужого или несуществующего счёта в ответ просто не попадает — ошибки нет: пустой items значит, что такого счёта у вашего приложения нет. invoice_ids сочетается со status и asset: invoice_ids=…&status=paid вернёт только оплаченные из перечисленных.

Весь список — постранично: count=1000&offset=0, count=1000&offset=1000, count=1000&offset=2000… (шаг offset равен count), пока не придёт пустой items. Архив листайте без status и фильтруйте у себя: счёт, сменивший статус во время листания, со status может не попасть ни на одну страницу. Новые счета сдвигают страницы — счёт может прийти дважды, сверяйте по invoice_id.

Ответ 400 с причиной: параметр не из таблицы выше, номер не из цифр, invoice_ids без номеров, больше 500 номеров, invoice_ids[] или invoice_id вместо invoice_ids, status не из списка, незнакомая монета в asset, fiat (счетов в фиате нет), отрицательный offset.

Суммы — те же, что в вебхуке invoice_paid: target — сколько выставлено, received — сколько пришло всего, amount у оплаченного счёта — на какую сумму он закрыт (номинал или, если счёт закрылся меньшей суммой, сколько пришло). У открытого и истёкшего счёта amount — номинал.

POSTtransfer

Приложение платит своему пользователю. Главный метод.

ПолеТипОбяз.Смысл
user_idстрокадачисловой Telegram id получателя
assetстрокадамонета
amountстрокадасумма, не меньше 0.1 $
spend_idстрокадаваш уникальный номер выплаты
commentстроканетдля вашей истории; получателю не показывается
{"transfer_id": 128, "spend_id": "traffy-payout-8891", "user_id": "247946877", "asset": "USDT", "amount": "12.40", "status": "completed", "completed_at": "2026-08-25T02:31:07+00:00", "reverted_at": null, "notified": true, "claim_url": "https://t.me/goswap?start=pay_k3JxQ9…", "already": false}

Получатель может ни разу не открывать бота. Telegram запрещает боту писать такому человеку первым, поэтому выплата ведёт себя двояко:

claim_url есть в ответе всегда: строка в нашей базе не гарантирует, что человеку можно написать (он мог заблокировать бота), а ссылка работает из любого состояния. Ссылка адресная — открывший её с другого аккаунта денег не получит. Пока выплата pending, её можно отозвать — revokeTransfer.

Про spend_id — это про ваши деньги. Сеть теряет ответы: вы отправили запрос, соединение оборвалось — прошла выплата или нет? Повторите запрос с тем же spend_id: деньги не уйдут дважды, вернётся прежний результат с already: true. Идемпотентность сильнее суточного лимита: повтор выполненной выплаты не отнимает лимит.

Право на переводы выдаётся отдельно и по умолчанию выключено: утёкший токен с ним выносит весь баланс. Есть и суточный потолок в долларах. spend_id привязан к вашему приложению — два приложения вправе прислать одинаковый номер.

POSTrevokeTransfer

Отзыв невостребованной выплаты по вашему spend_id: деньги возвращаются на счёт организации. Возможен только пока status: pending — человек бота ещё не открыл. Открыл — деньги его, ответ 400.

ПолеТипОбяз.Смысл
spend_idстрокаданомер выплаты, которую отзываете
{"transfer_id": 128, "spend_id": "traffy-payout-8891", "user_id": "247946877", "asset": "USDT", "amount": "12.40", "status": "reverted", "completed_at": null, "reverted_at": "2026-09-18T10:02:41+00:00", "notified": false, "claim_url": null, "already": false}

Повтор безопасен: already: true, денег не двигает. spend_id после отзыва занят навсегда — новую выплату тому же человеку делайте с новым номером. Суточный лимит отзыв не возвращает. Право нужно то же, что на transfer.

GETgetTransfers

История выплат. Параметры: spend_id (можно через запятую), count, offset. Нужен не для отчётности, а для восстановления: потеряли ответ на transfer — спросите по своему spend_id. Пусто — выплаты не было. У каждой выплаты статус: pending — ждёт человека, её можно отозвать; completed — получена; reverted — отозвана.

GETgetCurrencies

Что шлюз умеет.

[{"code": "USDT", "decimals": 6, "can_withdraw": true, "can_transfer": true}, {"code": "BTC", "decimals": 8, "can_withdraw": false, "can_transfer": true}]
can_withdraw: false — монету можно держать и передавать внутри, но вывести в цепь нельзя. Не стройте на такой монете экран вывода — ваш клиент узнает правду раньше вас.

GETgetExchangeRates

Курсы к доллару.

[{"source": "TRX", "target": "USD", "rate": "0.3431", "is_valid": true}]

Это справка, а не котировка сделки: значение из кеша отчётности, живёт минутами. is_valid: false — биржа недоступна, показано последнее известное. Считать по этому курсу деньги нельзя.

Вебхуки

Когда счёт оплачен, шлём POST на ваш адрес (https, задаётся в GoPay). Каждый запрос подписан — проверяйте подпись, она описана ниже.

{"update_type": "invoice_paid", "request_date": "2026-08-25T02:31:07+00:00", "payload": {"invoice_id": 7304918265, "hash": "kO6EXk…", "asset": "USDT", "amount": "49.99", "received": "49.99", "target": "50", "fee_asset": "USDT", "fee_amount": "0", "status": "paid", "paid_at": "…", "payload": "traffy-order-991"}}

target — сколько выставлено, received — сколько пришло, amount — на какую сумму счёт оплачен: номинал, а если пришло меньше — received.

Счёт оплачен на amount, а не на сумму вашего заказа: пополнение зачисляйте на amount, а товар с ценой отдавайте, сверив с ней amount. Перевод на адрес счёта (все сети, кроме TRON) закрывает счёт любой суммой от $0.5 — и в срок, и после истечения.

Переводы меньше $0.5 не зачисляются. BNB и ETH считаем в доллары по курсу; курса нет — счёт ждёт доплаты. В TRON счёт закрывает только полная сумма.

Ещё два события по счёту, с теми же полями:

Другая монета на адрес счёта (например, BNB на счёт в USDT) зачисляется на баланс организации как пополнение, счёт не закрывает, вебхука о ней нет.

Подпись

Заголовок crypto-pay-api-signature — HMAC-SHA256 от сырого тела на ключе sha256(ваш_токен):

import hashlib, hmac def valid(token: str, body: bytes, signature: str) -> bool: secret = hashlib.sha256(token.encode()).digest() mine = hmac.new(secret, body, hashlib.sha256).hexdigest() return hmac.compare_digest(mine, signature)

Считайте от тела, как оно пришло, до разбора JSON: пересобранный из объекта JSON даст другие байты и другую подпись.

Повторы

Не ответили 2xx — повторим: 17 попыток за трое суток, с растущими паузами.

Вебхук может прийти несколько раз, в том числе после того, как вы его обработали: ответ мог потеряться по дороге. Считайте обработанным по invoice_id и молча отвечайте 200 на повтор. Начислить дважды по одному счёту — ваш убыток.

Порядок внедрения

1. getMe — проверить токен.
2. createInvoice с тестовой суммой, оплатить самому, поймать вебхук.
3. Проверить подпись. Без проверки любой, кто узнал ваш адрес, начислит себе сколько захочет.
4. Попросить включить can_transfer и выставить суточный лимит.
5. transfer на свой же id, повторить с тем же spend_id — убедиться, что деньги ушли один раз.
6. Развести обработку по payload и spend_id на своей стороне.

Чего в API нет намеренно

Овердрафта и отсрочки. Отрицательный баланс приложения — это наши деньги у тысячи его пользователей, взыскать которые не с кого.

Возврата полученного платежа. Пришедшее пользователю принадлежит пользователю: отозвать можно только выплату, которую он ещё не получил (revokeTransfer). Нужен возврат — сделайте его обычной выплатой через transfer: он останется в истории обеих сторон.

Приёма фиата. Рубли — это лицензия и чарджбэки, а не ещё один метод оплаты. Шлюз работает с криптовалютой.