Документация API
Один API для всех способов оплаты. Подключение занимает вечер, а по сути — три шага.
1
Создайте счёт
Один запрос POST /v1/invoices с суммой и номером заказа. В ответе — ссылка на страницу оплаты.
2
Отправьте покупателя
Покупатель открывает ссылку, выбирает способ и платит. Страница оплаты — наша, вам её делать не нужно.
3
Получите уведомление
Мы пришлём POST на ваш адрес, вы проверите статус счёта — и выдаёте товар.
выдаётся при подключении
— все пути ниже добавляются к нему. Только 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-Signature | HMAC-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
const crypto = require("crypto"); // Node.js 18+, fetch встроен
const BASE = "https://адрес-api"; // адрес API
const KEY = "mer_7f3a9c21d04e8b65"; // ваш hub_key из сообщения в Telegram
const SECRET = "ваш hub_secret"; // только на сервере
async function call(method, path, body) {
const raw = body ? JSON.stringify(body) : ""; // тело ровно в том виде, в каком отправим
const ts = Math.floor(Date.now() / 1000).toString(); // время отправки, секунды
const nonce = crypto.randomBytes(16).toString("hex"); // случайная строка, у каждого запроса своя
const hash = crypto.createHash("sha256").update(raw).digest("hex"); // хеш тела
const msg = [method, path, ts, nonce, hash].join("\n"); // пять строк для подписи
const sig = crypto.createHmac("sha256", SECRET).update(msg).digest("hex"); // подпись секретом
const res = await fetch(BASE + path, { method, body: raw || undefined, headers: {
"X-Hub-Key": KEY, "X-Hub-Timestamp": ts, "X-Hub-Nonce": nonce,
"X-Hub-Signature": sig, "Content-Type": "application/json" } });
return res.json();
}
// 1. Счёт на 500 рублей за заказ order-1001
const invoice = await call("POST", "/v1/invoices", { order_id: "order-1001", amount: 500, currency: "rub" });
console.log(invoice.pay_url); // сюда отправляем покупателя
// 2. Позже — проверить, оплачен ли
const status = await call("GET", "/v1/invoices/" + invoice.invoice_id);
console.log(status.status); // pending → paid
<?php
define("BASE", "https://адрес-api"); // адрес API
define("KEY", "mer_7f3a9c21d04e8b65"); // ваш hub_key из сообщения в Telegram
define("SECRET", "ваш hub_secret"); // только на сервере
function call($method, $path, $body = null) {
$raw = $body === null ? "" : json_encode($body); // тело ровно в том виде, в каком отправим
$ts = (string) time(); // время отправки, секунды
$nonce = bin2hex(random_bytes(16)); // случайная строка, у каждого запроса своя
$msg = implode("\n", [$method, $path, $ts, $nonce, hash("sha256", $raw)]); // пять строк для подписи
$sig = hash_hmac("sha256", $msg, SECRET); // подпись секретом
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_POSTFIELDS => $raw,
CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [
"X-Hub-Key: " . KEY, "X-Hub-Timestamp: $ts", "X-Hub-Nonce: $nonce",
"X-Hub-Signature: $sig", "Content-Type: application/json"]]);
return json_decode(curl_exec($ch), true);
}
// 1. Счёт на 500 рублей за заказ order-1001
$invoice = call("POST", "/v1/invoices", ["order_id" => "order-1001", "amount" => 500, "currency" => "rub"]);
echo $invoice["pay_url"]; // сюда отправляем покупателя
// 2. Позже — проверить, оплачен ли
$status = call("GET", "/v1/invoices/" . $invoice["invoice_id"]);
echo $status["status"]; // pending → paid
Что увидите в консоли
https://payapi.pro/pay/inv_6e852b738ce33a84f3c4c5e4 pending
Получили 401 или 403 вместо ссылки — смотрите код ошибки в разделе «Ошибки»: чаще всего это часы сервера (stale_or_bad_timestamp) или тело, изменённое после подсчёта хеша (bad_signature).
/v1/invoicesСоздать счёт
Счёт — это «покупатель должен вам столько-то за такой-то заказ». Счёт живёт 2 часа. Повторный запрос с тем же order_id вернёт тот же счёт ("reused": true) — двойной оплаты за один заказ не будет, даже если ваш код отправит запрос дважды.
| Поле | Тип | Описание | |
|---|---|---|---|
| order_id | string | обязательно | Номер заказа в вашей системе. Повтор с тем же номером вернёт тот же счёт |
| amount | number | обязательно | Сумма счёта в валюте currency, больше нуля |
| currency | string | необязательно | Валюта счёта: usd (по умолчанию), rub, usdt, eur, try и другие — список в разделе «Способы и валюты» |
| method | string | необязательно | Способ оплаты (например crypto_usdt, sbp). Не указан — покупатель выберет сам на странице оплаты; тогда валюта счёта только usd, usdt или rub |
| sub_account | string | необязательно | Ваша метка для учёта (магазин, бот, канал): латиница, цифры, _ и -, до 64 символов. Вернётся в уведомлении и в истории |
| return_url | string | необязательно | Куда вернуть покупателя после оплаты |
Ответ, если способ не указан — выберет покупатель
{
"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.
/v1/invoices/{invoice_id}Статус счёта
Единственный источник правды об оплате. Уведомление говорит «посмотрите», статус говорит «оплачено». Товар выдавайте по статусу.
pendingpaidunderpaidexpiredcancelledПример ответа
{
"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.paidinvoice.underpaidinvoice.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, покажите покупателю список способов у себя и сообщите нам его выбор. В ответ получите реквизиты и покажете их сами.
/v1/invoices/{invoice_id}/methodsСпособы, доступные для этого счёта по вашему тарифу. Ответ: {"invoice_id": "…", "status": "pending", "chosen": null, "amount": 500.0, "currency": "rub", "methods": ["crypto_usdt", "sbp", "card_rub"]}. Если способ уже выбран — chosen заполнен, methods пуст.
/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.
/v1/merchants/{hub_key}/balanceБаланс по валютам и итоги. Ответ: {"merchant": "mer_…", "balance": {"usdt": "124.50", "rub": "0"}, "totals": {"usdt": {"credited": "…", "withdrawn": "…", "payments": 37, "balance": "…"}}}.
/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"}, …]}.
/v1/merchants/{hub_key}/notify_url · /v1/merchants/{hub_key}/fee_payerТекущий адрес уведомлений и кто платит комиссию (merchant — вы, buyer — покупатель доплачивает, а вы получаете ровно сумму счёта).
/v1/tariffsПубличные тарифы: цена, комиссия, способы. Без подписи.
Способы оплаты и валюты
Код способа — в поле 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_btcBitcoincrypto_ethEthereumcrypto_ltcLitecoincrypto_trxTRON (TRX)crypto_bnbBNBcrypto_tonGram (TON)Валюты счёта (поле currency):
usdusdtrubeurtrygbpuahbynkztuzsgelamdkgstjsplncnykrwthbmyridrinraedngnbdtМультибот: ваш бот работает через нас
Мультибот — это ваш собственный бот (ваш код, ваш сервер), который работает сразу на многих ботах-зеркалах Telegram. Вы меняете в коде две вещи: адрес Telegram API и токен. Всё остальное — как раньше.
Адрес API
https://mipay.bet/tgapiвместо https://api.telegram.org
Токен
12:AbC… (35 символов после двоеточия)выдаётся в кабинете, вкладка «Мультибот», вместо токена BotFather
Одна строка для вашей библиотеки
| Библиотека | Как указать адрес |
|---|---|
| aiogram 3 | Bot(token=TOKEN, session=AiohttpSession(api=TelegramAPIServer.from_base("https://mipay.bet/tgapi"))) |
| aiogram 2 | Bot(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-api | new TelegramBot(TOKEN, { polling: true, baseApiUrl: "https://mipay.bet/tgapi" }) |
| go-telegram-bot-api | tgbotapi.NewBotAPIWithAPIEndpoint(TOKEN, "https://mipay.bet/tgapi/bot%s/%s") |
| Java TelegramBots | options.setBaseUrl("https://mipay.bet/tgapi/") у DefaultBotOptions (v5–6) или TelegramUrl в OkHttpTelegramClient (v7+) |
| C# Telegram.Bot | new TelegramBotClient(new TelegramBotClientOptions(TOKEN, baseUrl: "https://mipay.bet/tgapi")) |
| Rust teloxide | Bot::new(TOKEN).set_api_url(Url::parse("https://mipay.bet/tgapi").unwrap()) |
| PHP Nutgram | new Nutgram(TOKEN, new Configuration(apiUrl: "https://mipay.bet/tgapi")) |
| Любой HTTP | https://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 | Код | Что значит |
|---|---|---|
| 400 | bad_json | тело запроса — не JSON |
| 400 | missing_params | не хватает обязательных полей |
| 400 | bad_amount | неверная сумма |
| 400 | bad_sub_account | недопустимая метка sub_account |
| 400 | bad_fee_payer | допустимо merchant или buyer |
| 400 | url_too_long | слишком длинный адрес |
| 400 | insufficient_funds | на балансе недостаточно средств для вывода |
| 409 | withdrawal_pending | уже есть открытая заявка на вывод — новая после выплаты или отклонения прежней |
| 401 | no_key | нет заголовка X-Hub-Key |
| 401 | missing_headers | не хватает заголовков подписи |
| 401 | unknown_key | ключ не найден |
| 401 | stale_or_bad_timestamp | время запроса расходится больше чем на 60 секунд |
| 403 | bad_signature | подпись не совпала |
| 403 | client_disabled | доступ приостановлен |
| 404 | not_found | счёт не найден |
| 404 | merchant_not_found | мерчант не найден |
| 409 | replay | повтор nonce — запрос уже был |
| 410 | invoice_expired | срок счёта истёк |
| 410 | invoice_closed | счёт закрыт |
| 429 | rate_limited | слишком много запросов — повторите позже |
| 503 | no_gateway_for_method | способ временно недоступен — попробуйте другой |
| 400 | currency_not_supported | валюта не поддерживается (без способа оплаты — только usd, usdt, rub) |
| 400 | amount_below_minimum | сумма ниже минимума способа; минимум — в поле minimum |
| 403 | method_not_in_tariff | способ не входит в ваш тариф; доступные — в поле allowed_methods |
| 400 | fee_payer_unsupported | для этого способа комиссию покупателем переложить нельзя |
| 400 | bad_notify_url | адрес уведомлений не подходит: только https, публичный хост |
| 503 | rate_unavailable | курс временно недоступен — повторите позже |