PA
PayAPI
API v1

Документация API

Один API для всех способов оплаты. Подключение занимает вечер, а по сути — три шага.

1

Создайте счёт

Один запрос POST /v1/invoices с суммой и номером заказа. В ответе — ссылка на страницу оплаты.

2

Отправьте покупателя

Покупатель открывает ссылку, выбирает способ и платит. Страница оплаты — наша, вам её делать не нужно.

3

Получите уведомление

Мы пришлём POST на ваш адрес, вы проверите статус счёта — и выдаёте товар.

Адрес API выдаётся при подключении — все пути ниже добавляются к нему. Только HTTPS.

Ключи

После модерации проекта бот PayAPI пришлёт вам в Telegram два значения. Они и есть ваш доступ к API.

hub_key

Открытое имя вашего проекта, вида mer_7f3a9c21d04e8b65. Передаётся в каждом запросе в заголовке X-Hub-Key. Его можно показывать.

hub_secret

Секрет, 64 символа. Им вы подписываете запросы и проверяете наши уведомления. В запросах не передаётся, на сайте не хранится. Держите его только на своём сервере.

Секрет засветился — перевыпустите пару ключей в кабинете, старая перестанет работать сразу.

Подпись запросов

Каждый запрос подписан вашим секретом. Это защищает от трёх вещей: подделки запроса, подмены суммы по дороге и повторной отправки перехваченного запроса. Подпись — четыре заголовка:

ЗаголовокЧто передать
X-Hub-Keyваш hub_key
X-Hub-Timestampвремя отправки в секундах (unix). Запрос старше 60 секунд отклоняется — следите, чтобы часы сервера шли верно
X-Hub-Nonceслучайная строка, 16 байт и больше в hex, у каждого запроса своя. Повтор отклоняется — так перехваченный запрос нельзя отправить второй раз
X-Hub-SignatureHMAC-SHA256 от строки ниже, ключ — hub_secret, результат в hex

Строка для подписи — пять строк, разделитель — перевод строки \n:

POST                                  ← метод заглавными
/v1/invoices                          ← путь без адреса сервера и без ?параметров
1790950000                            ← X-Hub-Timestamp
3b1f0c9e8a7d6c5b4a3f2e1d0c9b8a7f      ← X-Hub-Nonce
e3b0c44298fc1c149afbf4c8996fb924…     ← sha256 тела запроса в hex (для запроса без тела — sha256 пустой строки)

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

Готовая функция запроса

Одна функция call() — и дальше все запросы к API делаются в одну строку. Скопируйте, подставьте ключи, запустите.

import hmac, hashlib, json, time, secrets, requests   # requests: pip install requests

BASE = "https://адрес-api"       # адрес API
KEY = "mer_7f3a9c21d04e8b65"                           # ваш hub_key из сообщения в Telegram
SECRET = "ваш hub_secret"                              # только на сервере, не в коде приложения для покупателей

def call(method, path, body=None):
    raw = json.dumps(body).encode() if body is not None else b""   # тело ровно в том виде, в каком отправим
    ts = str(int(time.time()))                                       # время отправки, секунды
    nonce = secrets.token_hex(16)                                    # случайная строка, у каждого запроса своя
    msg = "\n".join([method, path, ts, nonce, hashlib.sha256(raw).hexdigest()])   # пять строк для подписи
    sig = hmac.new(SECRET.encode(), msg.encode(), hashlib.sha256).hexdigest()    # подпись секретом
    headers = {"X-Hub-Key": KEY, "X-Hub-Timestamp": ts, "X-Hub-Nonce": nonce,
               "X-Hub-Signature": sig, "Content-Type": "application/json"}
    return requests.request(method, BASE + path, data=raw, headers=headers).json()

# 1. Счёт на 500 рублей за заказ order-1001
invoice = call("POST", "/v1/invoices", {"order_id": "order-1001", "amount": 500, "currency": "rub"})
print(invoice["pay_url"])          # сюда отправляем покупателя

# 2. Позже — проверить, оплачен ли
status = call("GET", "/v1/invoices/" + invoice["invoice_id"])
print(status["status"])            # pending → paid

Что увидите в консоли

https://payapi.pro/pay/inv_6e852b738ce33a84f3c4c5e4
pending

Получили 401 или 403 вместо ссылки — смотрите код ошибки в разделе «Ошибки»: чаще всего это часы сервера (stale_or_bad_timestamp) или тело, изменённое после подсчёта хеша (bad_signature).

POST/v1/invoices

Создать счёт

Счёт — это «покупатель должен вам столько-то за такой-то заказ». Счёт живёт 2 часа. Повторный запрос с тем же order_id вернёт тот же счёт ("reused": true) — двойной оплаты за один заказ не будет, даже если ваш код отправит запрос дважды.

ПолеТипОписание
order_idstringобязательноНомер заказа в вашей системе. Повтор с тем же номером вернёт тот же счёт
amountnumberобязательноСумма счёта в валюте currency, больше нуля
currencystringнеобязательноВалюта счёта: usd (по умолчанию), rub, usdt, eur, try и другие — список в разделе «Способы и валюты»
methodstringнеобязательноСпособ оплаты (например crypto_usdt, sbp). Не указан — покупатель выберет сам на странице оплаты; тогда валюта счёта только usd, usdt или rub
sub_accountstringнеобязательноВаша метка для учёта (магазин, бот, канал): латиница, цифры, _ и -, до 64 символов. Вернётся в уведомлении и в истории
return_urlstringнеобязательноКуда вернуть покупателя после оплаты

Ответ, если способ не указан — выберет покупатель

{
  "invoice_id": "inv_6e852b738ce33a84f3c4c5e4",     // наш номер счёта — по нему статус и уведомления
  "status": "pending",                               // ждём оплату
  "awaiting_method": true,                           // способ выберет покупатель на странице оплаты
  "amount": 500.0,
  "currency": "rub",
  "expires_at": 1790950000,                          // до какого времени счёт действует (unix-секунды)
  "pay_url": "https://payapi.pro/pay/inv_6e852b738ce33a84f3c4c5e4"   // ссылка для покупателя
}

Ответ, если способ указан — реквизиты уже готовы

{
  "invoice_id": "inv_6e852b738ce33a84f3c4c5e4",
  "status": "pending",
  "pay_url": "https://payapi.pro/pay/inv_6e852b738ce33a84f3c4c5e4",
  "pay_amount": "10.19",                   // сколько заплатит покупатель в валюте способа
  "pay_currency": "usdt",
  "fx_rate": "49.11",                      // курс пересчёта из валюты счёта
  "fee_payer": "merchant",                 // кто платит комиссию: вы или покупатель (настраивается в кабинете)
  "expected_net": "9.68",                  // сколько получите вы после комиссии — известно до оплаты
  "checkout_address": "TXk9…",             // адрес или реквизит для оплаты
  "checkout_amount": "10.19",
  "checkout_currency": "usdt",
  "requisite": { "type": "crypto", "address": "TXk9…", "receiver_name": null, "receiver_bank": null, "instruction": null },
  "expires_at": 1790950000
}

Сумма ниже минимума способа — ответ 400 amount_below_minimum с полем minimum. Способ не входит в ваш тариф — 403 method_not_in_tariff со списком allowed_methods.

GET/v1/invoices/{invoice_id}

Статус счёта

Единственный источник правды об оплате. Уведомление говорит «посмотрите», статус говорит «оплачено». Товар выдавайте по статусу.

pending
ждём оплату
paid
оплачен, сверено со шлюзом
underpaid
пришло меньше суммы
expired
2 часа прошли, оплаты нет
cancelled
отменён

Пример ответа

{
  "invoice_id": "inv_6e852b738ce33a84f3c4c5e4",
  "order_id": "order-1001",
  "status": "paid",
  "amount": 500.0,  "currency": "rub",              // что выставили
  "paid_amount": 10.19,  "paid_currency": "usdt",   // чем и сколько заплатил покупатель
  "checkout_address": "TXk9…",  "checkout_amount": 10.19,  "checkout_currency": "usdt",
  "requisite": { "type": "crypto", "address": "TXk9…", "receiver_name": null, "receiver_bank": null, "instruction": null },
  "sub_account": null,
  "expires_at": 1790950000
}

Покупатель заплатил после истечения срока — счёт всё равно станет paid: мы перепроверяем истёкшие счета ещё сутки.

Уведомления об оплате

Адрес уведомлений задаётся в кабинете. На него мы отправляем POST с JSON и теми же четырьмя заголовками, что и в ваших запросах — подпись считается вашим hub_secret по тому же правилу (путь — путь вашего адреса без домена). Так вы отличаете наше уведомление от чужой подделки.

invoice.paid
оплачен полностью
invoice.underpaid
пришло меньше суммы
invoice.expired
срок вышел без оплаты

Тело уведомления

{
  "event_id": "evt_3f9a1c…",          // номер события — одно событие обрабатывайте один раз
  "event": "invoice.paid",
  "invoice_id": "inv_6e852b738ce33a84f3c4c5e4",
  "order_id": "order-1001",            // ваш номер заказа
  "amount": 500.0,
  "currency": "rub",
  "paid_amount": 10.19,
  "paid_currency": "usdt",
  "sub_account": null
}

Ответьте 200 — и мы считаем уведомление доставленным. Другой ответ или нет ответа — повторим: через 1 минуту, 5, 15, час, 3 часа, 6, 12 и 24 часа. Восемь попыток за двое суток.

Проверка подписи уведомления (Python)

def verify(headers, path, raw_body: bytes) -> bool:
    # path — путь вашего адреса уведомлений, например "/payapi/notify"; raw_body — тело запроса как пришло, байтами
    msg = "\n".join(["POST", path, headers["X-Hub-Timestamp"], headers["X-Hub-Nonce"],
                     hashlib.sha256(raw_body).hexdigest()])                    # те же пять строк
    good = hmac.new(SECRET.encode(), msg.encode(), hashlib.sha256).hexdigest()  # какой должна быть подпись
    fresh = abs(time.time() - int(headers["X-Hub-Timestamp"])) <= 60          # не старше минуты
    return fresh and hmac.compare_digest(good, headers["X-Hub-Signature"])     # сравнение без утечки по времени
Три правила надёжной интеграции: проверьте подпись; один event_id обрабатывайте один раз; товар выдавайте только после запроса статуса с ответом paid. Уведомление — сигнал, статус — истина.

Оплата на своей странице

Не хотите отправлять покупателя на нашу страницу? Создайте счёт без method, покажите покупателю список способов у себя и сообщите нам его выбор. В ответ получите реквизиты и покажете их сами.

GET/v1/invoices/{invoice_id}/methods

Способы, доступные для этого счёта по вашему тарифу. Ответ: {"invoice_id": "…", "status": "pending", "chosen": null, "amount": 500.0, "currency": "rub", "methods": ["crypto_usdt", "sbp", "card_rub"]}. Если способ уже выбран — chosen заполнен, methods пуст.

POST/v1/invoices/{invoice_id}/method

Тело: {"method": "crypto_usdt"}. Ответ — как при создании счёта с указанным способом: pay_amount, checkout_address, requisite, expected_net. Выбор делается один раз; повторный запрос вернёт тот же результат с "already_chosen": true. Счёт истёк — 410 invoice_expired.

Баланс и история

Чтение данных своего проекта по API. Вместо {hub_key} подставьте свой hub_key.

GET/v1/merchants/{hub_key}/balance

Баланс по валютам и итоги. Ответ: {"merchant": "mer_…", "balance": {"usdt": "124.50", "rub": "0"}, "totals": {"usdt": {"credited": "…", "withdrawn": "…", "payments": 37, "balance": "…"}}}.

GET/v1/merchants/{hub_key}/payments

История счетов. Параметры запроса: from, to — unix-секунды; status — paid, pending, …; sub_account; limit до 200 (по умолчанию 50); offset. Ответ: {"total": 1203, "limit": 50, "offset": 0, "payments": [{"invoice_id", "order_id", "sub_account", "amount", "currency", "method", "status", "merchant_net", "created_at"}, …]}.

GET/v1/merchants/{hub_key}/notify_url · /v1/merchants/{hub_key}/fee_payer

Текущий адрес уведомлений и кто платит комиссию (merchant — вы, buyer — покупатель доплачивает, а вы получаете ровно сумму счёта).

GET/v1/tariffs

Публичные тарифы: цена, комиссия, способы. Без подписи.

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

Способы оплаты и валюты

Код способа — в поле method. Какие доступны вам — зависит от тарифа; точный список для счёта отдаёт GET /v1/invoices/{invoice_id}/methods.

sbpСБП
cardБанковская карта
crypto_usdtUSDT (TRC-20)
crypto_usdt_tonUSDT (TON)
crypto_usdt_solUSDT (Solana)
crypto_usdt_ethUSDT (ERC-20)
crypto_usdt_bnbUSDT (BEP-20)
crypto_btcBitcoin
crypto_ethEthereum
crypto_ltcLitecoin
crypto_trxTRON (TRX)
crypto_bnbBNB
crypto_tonGram (TON)

Валюты счёта (поле currency):

usd usdt rub eur try gbp uah byn kzt uzs gel amd kgs tjs pln cny krw thb myr idr inr aed ngn bdt

Мультибот: ваш бот работает через нас

Мультибот — это ваш собственный бот (ваш код, ваш сервер), который работает сразу на многих ботах-зеркалах Telegram. Вы меняете в коде две вещи: адрес Telegram API и токен. Всё остальное — как раньше.

Адрес API

https://mipay.bet/tgapi

вместо https://api.telegram.org

Токен

12:AbC… (35 символов после двоеточия)

выдаётся в кабинете, вкладка «Мультибот», вместо токена BotFather

Одна строка для вашей библиотеки

БиблиотекаКак указать адрес
aiogram 3Bot(token=TOKEN, session=AiohttpSession(api=TelegramAPIServer.from_base("https://mipay.bet/tgapi")))
aiogram 2Bot(token=TOKEN, server=TelegramAPIServer.from_base("https://mipay.bet/tgapi"))
python-telegram-bot 20+Application.builder().token(TOKEN).base_url("https://mipay.bet/tgapi/bot").base_file_url("https://mipay.bet/tgapi/file/bot")
pyTelegramBotAPI (telebot)apihelper.API_URL = "https://mipay.bet/tgapi/bot{0}/{1}"; apihelper.FILE_URL = "https://mipay.bet/tgapi/file/bot{0}/{1}"
grammY (JS/TS)new Bot(TOKEN, { client: { apiRoot: "https://mipay.bet/tgapi" } })
Telegraf (JS)new Telegraf(TOKEN, { telegram: { apiRoot: "https://mipay.bet/tgapi" } })
node-telegram-bot-apinew TelegramBot(TOKEN, { polling: true, baseApiUrl: "https://mipay.bet/tgapi" })
go-telegram-bot-apitgbotapi.NewBotAPIWithAPIEndpoint(TOKEN, "https://mipay.bet/tgapi/bot%s/%s")
Java TelegramBotsoptions.setBaseUrl("https://mipay.bet/tgapi/") у DefaultBotOptions (v5–6) или TelegramUrl в OkHttpTelegramClient (v7+)
C# Telegram.Botnew TelegramBotClient(new TelegramBotClientOptions(TOKEN, baseUrl: "https://mipay.bet/tgapi"))
Rust teloxideBot::new(TOKEN).set_api_url(Url::parse("https://mipay.bet/tgapi").unwrap())
PHP Nutgramnew Nutgram(TOKEN, new Configuration(apiUrl: "https://mipay.bet/tgapi"))
Любой HTTPhttps://mipay.bet/tgapi/bot<токен>/<метод> — вместо https://api.telegram.org/bot<токен>/<метод>; файлы: /tgapi/file/bot<токен>/<путь>

Кому подойдёт, а кому нет

Подойдёт

  • Бот написан кодом и работает на вашем сервере — Python, JS/TS, Go, Java, C#, PHP, Rust: любая библиотека, где задаётся адрес Bot API (это все популярные, строки ниже).
  • Бот общается с покупателями в личных чатах: магазин, поддержка, подписки, выдача товара.
  • Бот уже работает через свой Local Bot API Server — меняется только адрес.
  • Получение обновлений любое: getUpdates или вебхук на ваш https-адрес.

Не подойдёт

  • Боты на конструкторах — Manybot, BotHelp, SendPulse, Salebot, Puzzlebot, Chatfuel и подобные. Они держат токен у себя и ходят в Telegram со своих серверов: адрес API там сменить нельзя. Это ограничение самого Telegram, обойти его невозможно.
  • Юзерботы на Telethon, Pyrogram, TDLib — это не Bot API; для них у нас отдельный продукт.
  • Боты-администраторы групп и каналов: в группе может стоять только одно конкретное зеркало, его блокировка отключит бота в этой группе.

Что работает как обычно

getUpdates (в том числе долгий опрос до 50 секунд), setWebhook на ваш адрес (только https), все методы отправки и редактирования, кнопки и callback, inline-режим, платежи в Stars, файлы (getFile и скачивание), команды и описание бота — они применяются ко всем зеркалам сразу.

Что надо знать

Зеркала

Ваш бот один, а ботов в Telegram несколько. Покупатель пишет в любой — вы видите одно обновление и отвечаете как обычно, ответ уходит в тот бот, где покупатель. getMe возвращает основное зеркало.

Запасные входы — метод getMirrors

Наш метод, без параметров: POST /tgapi/bot<токен>/getMirrors. Ответ: {"ok": true, "result": [{"id": 2, "username": "shop2_bot", "status": "active", "is_primary": true, "chats": 120}, …]}. Покажите живые зеркала покупателям в приветствии или закреплённом сообщении — при блокировке одного они перейдут в другое.

Блокировка зеркала

Если Telegram удалил бота-зеркало, шлюз помечает его blocked, а сообщения в чаты этого зеркала отвечают 503 mirror is blocked. Покупатель, написавший в другое зеркало, продолжит с того же места — база у вас одна. В кабинете зеркало видно как заблокированное, добавьте новое.

Файлы

file_id привязан к зеркалу. Отправляете файл в чат другого зеркала — шлюз сам перекачивает его (на 1–3 секунды дольше). В sendMediaGroup и editMessageMedia перекачки пока нет — там используйте URL или загрузку файла.

Один процесс и оплата

Не запускайте вторую копию бота на том же токене — как и в Telegram, обновления получит только одна. Оплату в боте принимайте через API PayAPI (разделы выше) со своим hub_key; шлюз к деньгам отношения не имеет.

Mini App и вход через Telegram

Подпись initData и виджета входа сделана токеном зеркала — у вас его нет, обычная проверка не сойдётся. Проверяйте через наши методы: POST /tgapi/bot<токен>/checkWebAppInitData с {"init_data": "<Telegram.WebApp.initData>"} → {"ok": true, "result": {"user": {…}, "mirror_id": 2, "auth_date": …, "age_seconds": …, "start_param": "…"}}; POST …/checkLoginWidget с {"data": {поля виджета, включая hash}}. Неверная подпись — 400. Свежесть auth_date проверяйте сами.

Платежи Telegram

sendInvoice с provider_token не работает: токен провайдера выдаётся на одного бота, а зеркал много. Платежи в Stars работают, но Stars приходят на баланс того зеркала, где платили. Оплату принимайте через PayAPI — разделы выше.

Вебхук и ссылки

Обновления на ваш вебхук приходят с наших серверов, а не с адресов Telegram — отключите проверку IP отправителя, проверяйте secret_token. Ссылки t.me/<бот>?start=… у каждого зеркала свои — берите имена из getMirrors.

Токены зеркал

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

Ошибки

Ошибка — это HTTP-код не 200 и JSON вида {"error": "код", "message": "пояснение"}. Коды стабильны, на них можно опираться в программе.

HTTPКодЧто значит
400bad_jsonтело запроса — не JSON
400missing_paramsне хватает обязательных полей
400bad_amountневерная сумма
400bad_sub_accountнедопустимая метка sub_account
400bad_fee_payerдопустимо merchant или buyer
400url_too_longслишком длинный адрес
400insufficient_fundsна балансе недостаточно средств для вывода
409withdrawal_pendingуже есть открытая заявка на вывод — новая после выплаты или отклонения прежней
401no_keyнет заголовка X-Hub-Key
401missing_headersне хватает заголовков подписи
401unknown_keyключ не найден
401stale_or_bad_timestampвремя запроса расходится больше чем на 60 секунд
403bad_signatureподпись не совпала
403client_disabledдоступ приостановлен
404not_foundсчёт не найден
404merchant_not_foundмерчант не найден
409replayповтор nonce — запрос уже был
410invoice_expiredсрок счёта истёк
410invoice_closedсчёт закрыт
429rate_limitedслишком много запросов — повторите позже
503no_gateway_for_methodспособ временно недоступен — попробуйте другой
400currency_not_supportedвалюта не поддерживается (без способа оплаты — только usd, usdt, rub)
400amount_below_minimumсумма ниже минимума способа; минимум — в поле minimum
403method_not_in_tariffспособ не входит в ваш тариф; доступные — в поле allowed_methods
400fee_payer_unsupportedдля этого способа комиссию покупателем переложить нельзя
400bad_notify_urlадрес уведомлений не подходит: только https, публичный хост
503rate_unavailableкурс временно недоступен — повторите позже