## Правила интеграции - Покупателя всегда отправляйте на `invoice.pay_url` — платёжную страницу Ketmus. Это касается и кнопок в Telegram-ботах, и редиректов на сайте. - Не отправляйте покупателю `payment_uri` или `payment.address` вместо `pay_url`. Для СБП (`rail: "sbp_qr"`) там прямая ссылка `https://qr.nspk.ru/…` — с ней покупатель не видит бренд Ketmus, сумму, таймер и статус оплаты и не возвращается в магазин. - Заказ выдавайте только по вебхуку `invoice.paid` с проверенной подписью `x-ketmus-signature`, а не по возврату покупателя на `success_url`. - API-ключ и секрет вебхуков храните только на сервере, в переменных окружения. --- # Обзор документации Как подключить Ketmus к любому проекту и начать принимать криптовалютные платежи. Документация предназначена для специалиста, который подключает Ketmus на стороне бизнеса: разработчика, технического специалиста или владельца проекта. Здесь описаны методы API, форматы запросов и ответов, уведомления о платежах и готовые сценарии для сайта, Telegram-бота и бизнеса без разработки. ## Что такое Ketmus [Ketmus](https://ketmus.com) — платёжный шлюз для онлайн-бизнеса, который принимает оплату в популярных криптовалютах и стейблкоинах: Bitcoin, Ethereum, USDT, USDC, TON, Solana и других, а также рублями по СБП — от клиентов по всему миру и без банковских ограничений. Каждый счёт получает собственный адрес для оплаты, а Ketmus сам отслеживает поступления в блокчейне, ждёт нужное число подтверждений, пересчитывает сумму по актуальному курсу и сообщает вашему серверу о результате через подписанный вебхук. ## Какой способ интеграции выбрать | Способ | Нужен разработчик | Когда подходит | Подробнее | | --- | --- | --- | --- | | Merchant API | Да | Полный контроль над оплатой: создание счетов, проверка статусов, автоматическая выдача товара | [Создание счёта](https://docs.ketmus.com/md/create-invoice.md) | | Платёжная страница | Минимально | Клиент оплачивает на готовой странице Ketmus, а затем возвращается на ваш сайт | [Платёжная страница](https://docs.ketmus.com/md/checkout-page.md) | | Telegram-бот | Да | Продажа подписок, доступа и цифровых товаров прямо в боте | [Telegram-бот](https://docs.ketmus.com/md/telegram-bot.md) | | Статические адреса | Да | Постоянный адрес пополнения для каждого клиента без создания счёта | [Статические адреса](https://docs.ketmus.com/md/static-addresses.md) | | Личный кабинет | Нет | Разовые счета и ссылки на оплату для клиентов, фрилансеров и B2B | [Для бизнеса](https://docs.ketmus.com/md/business.md) | ## Как начать принимать платежи 1. **Зарегистрируйтесь.** Создайте аккаунт в [личном кабинете Ketmus](https://app.ketmus.com/register/account). 2. **Создайте магазин.** В личном кабинете создайте новую интеграцию — это и есть магазин: укажите название, сайт или бота и выберите монеты, которые хотите принимать. 3. **Получите ключи.** Скопируйте API-ключ магазина и секрет вебхуков. → [Получение API-ключа](https://docs.ketmus.com/md/api-keys.md) 4. **Настройте уведомления.** Укажите URL, на который Ketmus будет отправлять вебхуки о статусах счетов. → [Вебхуки](https://docs.ketmus.com/md/webhooks.md) 5. **Создайте первый счёт.** Отправьте запрос `POST /invoices` и перенаправьте клиента на `pay_url`. → [Быстрый старт](https://docs.ketmus.com/md/quickstart.md) 6. **Проверьте интеграцию.** Отправьте тестовый вебхук из кабинета и проведите небольшой реальный платёж. → [Тестирование](https://docs.ketmus.com/md/testing.md) ## Возможности API - [Создание счёта](https://docs.ketmus.com/md/create-invoice.md) — выставить счёт клиенту и получить ссылку на оплату или реквизиты. - [Информация о счёте](https://docs.ketmus.com/md/get-invoice.md) — статус, полученные суммы и транзакции. - [Список счетов](https://docs.ketmus.com/md/list-invoices.md) — все счета магазина с фильтром по статусу. - [Отмена счёта](https://docs.ketmus.com/md/cancel-invoice.md) — отменить неоплаченный счёт. - [Статические адреса](https://docs.ketmus.com/md/static-addresses.md) — постоянный адрес пополнения для клиента. - [Баланс](https://docs.ketmus.com/md/balance.md) — балансы магазина по каждой монете. - [Монеты и сети](https://docs.ketmus.com/md/rails.md) — какие способы оплаты доступны магазину. - [Курсы](https://docs.ketmus.com/md/rates.md) — актуальные курсы криптовалют к USD и фиатным валютам. - [Вебхуки](https://docs.ketmus.com/md/webhooks.md) — уведомления об изменении статуса счёта на ваш сервер. ## Основная информация об API - Базовый URL: `https://app.ketmus.com/api/merchant/v1` - Формат данных — JSON, кодировка UTF-8. - Авторизация — API-ключ магазина в заголовке `Authorization: Bearer ` в каждом запросе. → [Авторизация](https://docs.ketmus.com/md/authorization.md) - Лимит — 300 запросов в минуту на один ключ. → [Ошибки и лимиты](https://docs.ketmus.com/md/errors.md) ## Основные понятия | Термин | Значение | | --- | --- | | Магазин | Отдельная точка приёма платежей — сайт, бот или приложение — со своим API-ключом, настройками и статистикой | | API-ключ | Секретный ключ магазина для авторизации запросов к API | | Секрет вебхуков | Ключ для проверки подписи уведомлений, которые Ketmus отправляет на ваш сервер | | Счёт (invoice) | Запрос на оплату с суммой, сроком жизни и уникальным идентификатором | | Rail | Способ оплаты: конкретная монета в конкретной сети, например `usdt_tron` или `btc` | | TXID | Хеш транзакции в блокчейне | | Вебхук | HTTP-уведомление, которое Ketmus отправляет на ваш URL при изменении статуса счёта | ## Где получить помощь Если у вас остались вопросы, напишите в поддержку через чат в [личном кабинете](https://app.ketmus.com) или на [сайте Ketmus](https://ketmus.com). Ответы на частые вопросы собраны в разделе [Поддержка и FAQ](https://docs.ketmus.com/md/faq.md). --- # Как работает интеграция Жизненный цикл платежа: от создания счёта до зачисления средств на баланс магазина. ## Схема платежа 1. **Ваш сервер создаёт счёт.** Вы отправляете `POST /invoices` с суммой, валютой цены и номером заказа. Ketmus возвращает объект счёта и ссылку `pay_url`. 2. **Клиент выбирает монету.** На платёжной странице клиент выбирает монету и сеть. Если вы передали `rail`, адрес выдаётся сразу и шаг пропускается. 3. **Ketmus выдаёт адрес.** Для счёта создаётся отдельный адрес, а сумма в криптовалюте фиксируется по текущему курсу. Счёт переходит в статус `pending`. 4. **Клиент отправляет перевод.** Как только транзакция появляется в сети, счёт переходит в `confirming` и ждёт нужное число подтверждений. 5. **Платёж подтверждён.** Счёт получает статус `paid` (или `paid_over` при переплате), сумма за вычетом комиссии зачисляется на баланс магазина. 6. **Вы получаете вебхук.** При каждом изменении статуса Ketmus отправляет подписанный POST-запрос на ваш URL. По событию `invoice.paid` вы выдаёте товар. ## Источник истины Не выдавайте товар по факту возврата клиента на `success_url` — пользователь может открыть этот адрес вручную. Решение о выдаче принимайте только по вебхуку с проверенной подписью или после запроса [информации о счёте](https://docs.ketmus.com/md/get-invoice.md) с вашего сервера. > **Важно.** Обрабатывайте вебхуки идемпотентно: одно и то же событие может прийти повторно. Сохраняйте `delivery_id` и не выдавайте товар дважды для одного `order_id`. ## Суммы и курс Цена счёта задаётся в одной из валют: `USD`, `USDT`, `EUR`, `RUB`, `UAH`, `KZT`, `TRY`, `GBP`. После выбора монеты Ketmus пересчитывает её в сумму к оплате `payment.amount_due` по актуальному курсу и фиксирует курс в поле `payment.rate` на всё время жизни счёта. Все суммы в криптовалюте передаются строками в десятичном виде, например `"0.00041"`, чтобы не терять точность. Не преобразуйте их в числа с плавающей точкой при сохранении в базу. ## Комиссия Комиссия задаётся в базисных пунктах (`bps`, 100 bps = 1%). Её может оплачивать магазин или клиент — параметр `fee_payer`. Если комиссию платит клиент, она добавляется к сумме к оплате; если магазин — удерживается из зачисления. Итоговые суммы видны в объекте `fee` счёта. ## Недоплата, переплата и поздние платежи - **Недоплата.** Если пришло меньше нужной суммы, счёт переходит в `wrong_amount_waiting` и ждёт доплаты до истечения срока. Параметр `underpaid_coverage_bps` позволяет считать счёт оплаченным при небольшой недоплате, например 50 bps = 0,5%. - **Переплата.** Если пришло больше, счёт получает статус `paid_over`, а на баланс зачисляется вся полученная сумма. - **Поздний платёж.** Если перевод пришёл после истечения счёта, Ketmus всё равно зачислит его и отправит событие `invoice.late_payment`. --- # Быстрый старт Первый счёт и обработка оплаты за 10 минут. В этом руководстве вы создадите счёт через API, отправите клиента на платёжную страницу и обработаете уведомление об оплате. ## 1. Сохраните API-ключ Получите ключ в личном кабинете ([как это сделать](https://docs.ketmus.com/md/api-keys.md)) и сохраните его в переменных окружения сервера. Никогда не передавайте ключ в браузер или в код мобильного приложения. ```bash KETMUS_API_KEY=XXXXXX-XXXXXX-XXXXXX-XXXXXX KETMUS_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` ## 2. Создайте счёт ```bash curl -X POST "https://app.ketmus.com/api/merchant/v1/invoices" \ -H "Authorization: Bearer $KETMUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 25, "currency": "USD", "order_id": "ORDER-1042", "description": "Подписка Pro на 1 месяц", "callback_url": "https://shop.example.com/webhooks/ketmus", "success_url": "https://shop.example.com/orders/1042?paid=1" }' ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 25, currency: 'USD', order_id: 'ORDER-1042', description: 'Подписка Pro на 1 месяц', callback_url: 'https://shop.example.com/webhooks/ketmus', success_url: 'https://shop.example.com/orders/1042?paid=1', }), }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.post( "https://app.ketmus.com/api/merchant/v1/invoices", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, json={ "amount": 25, "currency": "USD", "order_id": "ORDER-1042", "description": "Подписка Pro на 1 месяц", "callback_url": "https://shop.example.com/webhooks/ketmus", "success_url": "https://shop.example.com/orders/1042?paid=1", }, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => 25, 'currency' => 'USD', 'order_id' => 'ORDER-1042', 'description' => 'Подписка Pro на 1 месяц', 'callback_url' => 'https://shop.example.com/webhooks/ketmus', 'success_url' => 'https://shop.example.com/orders/1042?paid=1', ]), ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## 3. Перенаправьте клиента В ответе придёт объект `invoice`. Отправьте клиента на `invoice.pay_url` — там он выберет монету, увидит адрес, QR-код и таймер. После оплаты клиент вернётся на `success_url`. ```javascript const { invoice } = data // Сохраните invoice.id рядом с заказом, чтобы потом сверять вебхуки await db.orders.update({ id: 'ORDER-1042' }, { ketmusInvoiceId: invoice.id }) res.redirect(303, invoice.pay_url) ``` ## 4. Обработайте вебхук Когда счёт будет оплачен, Ketmus отправит POST-запрос на `callback_url`. Проверьте подпись и выдайте товар. Ниже — минимальный вариант; полная версия с проверкой времени `t` и примерами на Python и PHP — в разделе [Проверка подписи](https://docs.ketmus.com/md/webhook-signature.md): ```javascript import crypto from 'node:crypto' import express from 'express' const app = express() app.post('/webhooks/ketmus', express.raw({ type: 'application/json' }), async (req, res) => { const header = req.get('x-ketmus-signature') ?? '' const { t, v1 } = Object.fromEntries(header.split(',').map((part) => part.split('='))) const expected = crypto .createHmac('sha512', process.env.KETMUS_WEBHOOK_SECRET) .update(`${t}.${req.body}`) .digest('hex') const valid = v1 && expected.length === v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)) if (!valid) return res.status(400).send('bad signature') const event = JSON.parse(req.body) if (event.event === 'invoice.paid' || event.event === 'invoice.paid_over') { await fulfillOrder(event.order_id, event.delivery_id) } res.sendStatus(200) }) ``` > **Готово.** Это вся базовая интеграция. Дальше изучите [статусы счёта](https://docs.ketmus.com/md/invoice-statuses.md), [все события вебхуков](https://docs.ketmus.com/md/webhooks.md) и сценарии для [сайта](https://docs.ketmus.com/md/website.md) и [Telegram-бота](https://docs.ketmus.com/md/telegram-bot.md). --- # Получение API-ключа Где найти API-ключ и секрет вебхуков магазина и как их хранить. ## Где найти ключи 1. **Войдите в кабинет.** Откройте [личный кабинет](https://app.ketmus.com/login). Если магазина ещё нет, создайте новую интеграцию: укажите название и монеты для приёма. 2. **Скопируйте API-ключ.** В таблице **Merchant API-ключи** найдите свой магазин и нажмите кнопку копирования рядом с ключом. Ключ выглядит так: `K7P2QX-9DMW4R-T3ZB8N-H6VY5C`. 3. **Скопируйте секрет вебхуков.** Откройте магазин, перейдите на вкладку **Уведомления**, укажите URL для вебхуков и скопируйте **секрет подписи**. Он начинается с `whsec_`. ## Какие бывают ключи | Ключ | Формат | Где используется | Кому виден | | --- | --- | --- | --- | | API-ключ | `XXXXXX-XXXXXX-XXXXXX-XXXXXX` — 4 группы по 6 заглавных латинских букв и цифр | Заголовок `Authorization: Bearer <ключ>` во всех запросах к API | Только вашему серверу | | Секрет вебхуков | `whsec_…` | Проверка подписи `x-ketmus-signature` входящих уведомлений | Только вашему серверу | > Передавайте ключ целиком, вместе с дефисами и без пробелов. У каждого магазина свой ключ: счёт, созданный с ключом магазина, зачисляется на баланс именно этого магазина. ## Безопасное хранение - Храните ключи в переменных окружения или менеджере секретов, а не в коде и не в репозитории. - Не используйте ключ в браузере, мобильном приложении или в коде Telegram Mini App — все запросы к API отправляйте только со своего сервера. - Если ключ мог попасть к посторонним, сразу перевыпустите его в настройках магазина. Старый ключ перестанет работать. > **API-ключ даёт полный доступ к магазину.** Тот, у кого есть ключ, может создавать и отменять счета и видеть баланс. Относитесь к нему как к паролю. --- # Тестирование интеграции Как проверить интеграцию перед запуском: тестовый вебхук, проверка подписи и пробный платёж. ## Чек-лист перед запуском 1. **Проверьте авторизацию.** Вызовите `GET /me` — в ответе должно прийти название вашего магазина. 2. **Отправьте тестовый вебхук.** В настройках магазина нажмите кнопку отправки тестового вебхука. На ваш URL придёт событие `webhook.test` с корректной подписью. 3. **Создайте счёт.** Создайте счёт через API и откройте `pay_url` в браузере — убедитесь, что видите нужные монеты и сумму. 4. **Проведите небольшой платёж.** Оплатите счёт на минимальную сумму в сети с низкой комиссией: USDT (TRC-20), TON, USDT в TON или Polygon. 5. **Проверьте выдачу товара.** Убедитесь, что пришли события `invoice.pending`, `invoice.confirming` и `invoice.paid`, а заказ отмечен оплаченным ровно один раз. ## Проверка ключа ```bash curl "https://app.ketmus.com/api/merchant/v1/me" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/me', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/me", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Вебхуки на локальной машине Ketmus отправляет уведомления только на публичные адреса. Для разработки откройте локальный сервер через туннель (например, `ngrok http 3000` или `cloudflared tunnel`) и укажите полученный HTTPS-адрес в `callback_url`. ## Минимальные суммы У каждой сети есть минимальная сумма счёта, чтобы перевод не съела комиссия сети. Например, для BTC это 5 USD, для ETH и USDT в Ethereum — 10 USD, для большинства остальных сетей — 1–2 USD. Полный список — в разделе [Поддерживаемые монеты](https://docs.ketmus.com/md/coins.md). --- # ИИ-ассистенты и MCP Как подключить документацию Ketmus к ChatGPT, Claude, Claude Code, Cursor, VS Code и Codex, чтобы ИИ сам написал интеграцию. Документация Ketmus доступна в форматах, которые удобно читать языковым моделям. Подключите её к своему ИИ-ассистенту, и он будет писать код по актуальным методам API, а не по догадкам. ## Меню «Копировать» На каждой странице справа от заголовка есть кнопка **Копировать**. Она копирует страницу в Markdown, а стрелка рядом открывает меню: просмотр Markdown, открытие страницы в ChatGPT или Claude и подключение MCP-сервера. ## MCP-сервер MCP-сервер даёт ассистенту инструменты для поиска и чтения документации. Адрес сервера: `https://docs.ketmus.com/mcp` (транспорт Streamable HTTP, авторизация не нужна). | Инструмент | Что делает | | --- | --- | | `search_docs` | Ищет по документации и возвращает подходящие разделы со ссылками. | | `get_page` | Возвращает страницу целиком в Markdown по её `slug`, например `create-invoice` или `telegram-bot`. | | `list_pages` | Возвращает список всех страниц с описаниями. | ### Claude Code ```bash claude mcp add --transport http ketmus-docs https://docs.ketmus.com/mcp ``` ### Codex ```bash codex mcp add ketmus-docs --url https://docs.ketmus.com/mcp ``` ### Cursor Добавьте сервер в `.cursor/mcp.json` в корне проекта или в глобальные настройки MCP. ```json { "mcpServers": { "ketmus-docs": { "url": "https://docs.ketmus.com/mcp" } } } ``` ### VS Code Выберите **Подключиться к VSCode** в меню «Копировать» или добавьте сервер в `.vscode/mcp.json` вручную. ```json { "servers": { "ketmus-docs": { "type": "http", "url": "https://docs.ketmus.com/mcp" } } } ``` ### Claude и ChatGPT В Claude добавьте сервер в **Settings → Connectors → Add custom connector** с адресом `https://docs.ketmus.com/mcp`. В ChatGPT это делается в режиме разработчика через **Settings → Connectors**. Если подключать сервер не хочется, выберите в меню **Открыть в ChatGPT** или **Открыть в Claude**: ассистент откроет страницу и ответит по ней. ## llms.txt и Markdown - [https://docs.ketmus.com/llms.txt](https://docs.ketmus.com/llms.txt) — оглавление документации со ссылками на Markdown-версии страниц. - [https://docs.ketmus.com/llms-full.txt](https://docs.ketmus.com/llms-full.txt) — вся документация одним файлом. Его можно приложить к чату целиком. - `https://docs.ketmus.com/md/.md` — любая страница в Markdown, например [/md/create-invoice.md](https://docs.ketmus.com/md/create-invoice.md). ## Пример запроса к ассистенту ``` Используя MCP-сервер ketmus-docs, добавь в моего Telegram-бота на aiogram 3 команду /buy: - создавай счёт Ketmus на 9.99 USD с metadata.telegram_user_id; - отправляй пользователю кнопку только с pay_url, даже для СБП — не используй payment_uri и ссылки qr.nspk.ru; - прими вебхук, проверь подпись x-ketmus-signature и выдай доступ по invoice.paid один раз. Ключи бери из переменных окружения KETMUS_API_KEY, KETMUS_WEBHOOK_SECRET и BOT_TOKEN. ``` > **Не вставляйте ключи в чат.** Ассистенту не нужен ваш API-ключ или секрет вебхуков. Храните их в переменных окружения и передавайте коду, а не модели. --- # Авторизация запросов Как передавать API-ключ магазина и какой базовый URL использовать. ## Базовый URL ``` https://app.ketmus.com/api/merchant/v1 ``` Все методы принимают и возвращают JSON. Для методов с телом передавайте заголовок `Content-Type: application/json`. ## Заголовок авторизации Передавайте API-ключ магазина в заголовке `Authorization` с префиксом `Bearer`. Отдельный запрос для получения токена не нужен — ключ передаётся в каждом запросе. ``` Authorization: Bearer XXXXXX-XXXXXX-XXXXXX-XXXXXX ``` > Ключ магазина — из таблицы **Merchant API-ключи** в личном кабинете, вида `K7P2QX-9DMW4R-T3ZB8N-H6VY5C`. Подробнее — [Получение API-ключа](https://docs.ketmus.com/md/api-keys.md). Если ваш HTTP-клиент не позволяет задать `Authorization`, используйте альтернативный заголовок `X-API-Key: `. ## Пример запроса ```bash curl "https://app.ketmus.com/api/merchant/v1/me" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/me', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/me", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` Если ключ верный, в ответе придёт объект с названием магазина и его настройками — пример разобран в разделе [Информация о магазине](https://docs.ketmus.com/md/store.md). ## Ошибки авторизации | HTTP | code | Причина | | --- | --- | --- | | 401 | `unauthorized` | Ключ не передан, имеет неверный формат или не найден | | 403 | `inactive` | Магазин отключён | | 429 | `rate_limited` | Превышен лимит 300 запросов в минуту | --- # Ошибки и лимиты Формат ошибок API, коды ответов и ограничения на частоту запросов. ## Формат ошибки При ошибке API возвращает HTTP-код 4xx или 5xx и объект `error` с машинным кодом и понятным сообщением: ```json { "error": { "code": "invalid", "message": "amount: Number must be greater than 0" } } ``` ## Коды ошибок | HTTP | code | Что означает | Что делать | | --- | --- | --- | --- | | 400 | `invalid` | Неверные параметры или тело запроса не JSON | Исправьте запрос по сообщению в `message` | | 401 | `unauthorized` | Нет ключа или ключ неверный | Проверьте заголовок `Authorization` | | 403 | `inactive` | Магазин отключён | Обратитесь в поддержку | | 404 | `not_found` | Счёт или метод не найден | Проверьте идентификатор и путь | | 405 | `method_not_allowed` | Метод HTTP не поддерживается | Используйте GET или POST согласно документации | | 429 | `rate_limited` | Превышен лимит запросов | Повторите через минуту | | 500 | `internal` | Внутренняя ошибка | Повторите запрос позже | | 503 | `unavailable` | Сервис временно недоступен | Повторите с экспоненциальной задержкой | | 503 | `rail_unavailable` | Выбранная сеть временно недоступна | Предложите клиенту другую монету | ## Лимиты - Не более **300 запросов в минуту** на один API-ключ. - Не опрашивайте статус счёта в цикле — используйте [вебхуки](https://docs.ketmus.com/md/webhooks.md). Если опрос всё же нужен, делайте его не чаще раза в 15–30 секунд. - Для `GET /invoices` максимальный `limit` — 200 записей. ## Повторы запросов Повторяйте запросы только при ответах `429`, `500` и `503` или сетевой ошибке. Чтобы при повторе `POST /invoices` не создать дубликат счёта, передавайте свой `order_id` и перед повтором проверяйте, нет ли уже счёта с ним: `GET /invoices?order_id=`. --- # Создание счёта Создаёт счёт на оплату и возвращает ссылку на платёжную страницу и реквизиты. `POST /invoices` Создаёт новый счёт. Если передан `rail`, адрес для оплаты выдаётся сразу и его можно показать в своём интерфейсе. Без `rail` клиент выберет монету на платёжной странице `pay_url`. ## Параметры | Параметр | Тип | Обязательный | Описание | | --- | --- | --- | --- | | `amount` | number \| string | да | Сумма счёта в валюте `currency`. Больше 0, не более 1 000 000 000. | | `currency` | string | нет | Валюта цены: `USD`, `USDT`, `EUR`, `RUB`, `UAH`, `KZT`, `TRY`, `GBP`. По умолчанию `USD`. | | `order_id` | string | нет | Ваш номер заказа, до 64 символов. По нему можно найти счёт и избежать дубликатов. | | `description` | string | нет | Описание, которое увидит клиент на платёжной странице. До 300 символов. | | `metadata` | object | нет | Произвольные данные (например, `user_id`). Возвращаются в ответах и вебхуках. | | `rail` | string | нет | Сразу выбрать монету и сеть, например `usdt_tron`. Список — в [Монеты и сети](https://docs.ketmus.com/md/rails.md). | | `rails` | string[] | нет | Ограничить список монет, из которых выбирает клиент. | | `lifetime_sec` | integer | нет | Время жизни счёта в секундах: от 300 (5 минут) до 86 400 (24 часа). По умолчанию — из настроек магазина (обычно 3600). | | `fee_payer` | string | нет | Кто платит комиссию: `merchant` или `payer`. По умолчанию — из настроек магазина. | | `underpaid_coverage_bps` | integer | нет | Допустимая недоплата в базисных пунктах (0–5000), при которой счёт всё равно считается оплаченным. | | `callback_url` | string | нет | URL для вебхуков этого счёта. Если не передан — используется URL из настроек магазина. | | `return_url` | string | нет | Куда вернуть клиента по кнопке «Вернуться в магазин». | | `success_url` | string | нет | Куда перенаправить клиента после успешной оплаты. | | `fail_url` | string | нет | Куда перенаправить клиента, если счёт истёк или отменён. | | `payer_email` | string | нет | Email клиента для чека и связи. | | `customer_id` | string | нет | Ваш идентификатор клиента, до 120 символов. | > Неизвестные параметры отклоняются с ошибкой `400 invalid` — это защищает от опечаток в названиях полей. ## Пример запроса ```bash curl -X POST "https://app.ketmus.com/api/merchant/v1/invoices" \ -H "Authorization: Bearer $KETMUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 25, "currency": "USD", "order_id": "ORDER-1042", "description": "Подписка Pro на 1 месяц", "rail": "usdt_tron", "metadata": { "user_id": "u_731" }, "callback_url": "https://shop.example.com/webhooks/ketmus", "success_url": "https://shop.example.com/orders/1042?paid=1", "fail_url": "https://shop.example.com/orders/1042?failed=1" }' ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 25, currency: 'USD', order_id: 'ORDER-1042', description: 'Подписка Pro на 1 месяц', rail: 'usdt_tron', metadata: { user_id: 'u_731', }, callback_url: 'https://shop.example.com/webhooks/ketmus', success_url: 'https://shop.example.com/orders/1042?paid=1', fail_url: 'https://shop.example.com/orders/1042?failed=1', }), }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.post( "https://app.ketmus.com/api/merchant/v1/invoices", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, json={ "amount": 25, "currency": "USD", "order_id": "ORDER-1042", "description": "Подписка Pro на 1 месяц", "rail": "usdt_tron", "metadata": { "user_id": "u_731", }, "callback_url": "https://shop.example.com/webhooks/ketmus", "success_url": "https://shop.example.com/orders/1042?paid=1", "fail_url": "https://shop.example.com/orders/1042?failed=1", }, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => 25, 'currency' => 'USD', 'order_id' => 'ORDER-1042', 'description' => 'Подписка Pro на 1 месяц', 'rail' => 'usdt_tron', 'metadata' => [ 'user_id' => 'u_731', ], 'callback_url' => 'https://shop.example.com/webhooks/ketmus', 'success_url' => 'https://shop.example.com/orders/1042?paid=1', 'fail_url' => 'https://shop.example.com/orders/1042?failed=1', ]), ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Ответ HTTP `201 Created`. Объект счёта содержит также `available_rails` — монеты, которыми можно оплатить этот счёт. ```json { "invoice": { "type": "invoice", "id": "8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d", "order_id": "ORDER-1042", "status": "pending", "is_final": false, "late_payment": false, "price": { "amount": "25", "currency": "USD" }, "payment": { "rail": "usdt_tron", "asset": "USDT", "network": "tron", "address": "TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "memo": null, "amount_due": "25.31", "amount_received": "0", "amount_confirmed": "0", "late_amount": "0", "rate": "1", "txids": [] }, "fee": { "bps": 125, "payer": "payer", "amount": "0", "merchant_amount": "0" }, "pay_url": "https://app.ketmus.com/checkout/inv_5Kq2ZtR8wLmA", "metadata": { "user_id": "u_731" }, "created_at": "2026-11-03T10:15:00.000Z", "expires_at": "2026-11-03T11:15:00.000Z", "paid_at": null, "public_id": "inv_5Kq2ZtR8wLmA", "description": "Подписка Pro на 1 месяц", "lifetime_sec": 3600, "underpaid_coverage_bps": 0, "refresh_count": 0, "return_url": "https://shop.example.com/orders/1042", "success_url": "https://shop.example.com/orders/1042?paid=1", "fail_url": "https://shop.example.com/orders/1042?failed=1", "payer_email": null, "customer_id": null, "payment_uri": null, "explorer_address_url": "https://tronscan.org/#/address/TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "available_rails": [ { "id": "usdt_tron", "asset": "USDT", "network": "tron", "label": "USDT", "network_label": "Tron (TRC-20)", "icon": "/coins/tether.svg", "confirmations": 20 }, { "id": "ton", "asset": "TON", "network": "ton", "label": "Toncoin", "network_label": "TON", "icon": "/coins/toncoin.svg", "confirmations": 1 } ] } } ``` ### Счёт без выбранной монеты Если `rail` не передан, поле `payment` равно `null`, а статус — `new`. Адрес появится, когда клиент выберет монету на `pay_url`: ```json { "invoice": { "type": "invoice", "id": "8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d", "order_id": "ORDER-1042", "status": "new", "is_final": false, "late_payment": false, "price": { "amount": "25", "currency": "USD" }, "fee": null, "pay_url": "https://app.ketmus.com/checkout/inv_5Kq2ZtR8wLmA", "metadata": { "user_id": "u_731" }, "created_at": "2026-11-03T10:15:00.000Z", "expires_at": "2026-11-03T11:15:00.000Z", "paid_at": null, "public_id": "inv_5Kq2ZtR8wLmA", "description": "Подписка Pro на 1 месяц", "lifetime_sec": 3600, "underpaid_coverage_bps": 0, "refresh_count": 0, "return_url": "https://shop.example.com/orders/1042", "success_url": "https://shop.example.com/orders/1042?paid=1", "fail_url": "https://shop.example.com/orders/1042?failed=1", "payer_email": null, "customer_id": null, "payment_uri": null, "explorer_address_url": null, "payment": null } } ``` ## Поля объекта счёта | Поле | Тип | Описание | | --- | --- | --- | | `id` | string | Идентификатор счёта (UUID). Используйте в запросах к API. | | `public_id` | string | Публичный идентификатор для ссылки на оплату. | | `status` | string | Текущий статус. → [Статусы счёта](https://docs.ketmus.com/md/invoice-statuses.md) | | `is_final` | boolean | `true`, если статус окончательный и больше не изменится. | | `price` | object | Цена счёта: `amount` и `currency`. | | `payment` | object \| null | Реквизиты и суммы в выбранной монете. `null`, пока монета не выбрана. | | `payment.address` | string | Адрес, на который нужно отправить перевод. | | `payment.memo` | string \| null | Memo или destination tag (для XRP). Без него платёж не будет зачислен. | | `payment.amount_due` | string | Сумма к оплате в монете, включая комиссию, если её платит клиент. | | `payment.amount_received` | string | Сколько уже пришло, включая неподтверждённые транзакции. | | `payment.amount_confirmed` | string | Сколько пришло с нужным числом подтверждений. | | `payment.txids` | string[] | Хеши транзакций по счёту. | | `fee` | object \| null | Комиссия: `bps`, `payer`, `amount` и `merchant_amount` — сумма к зачислению магазину. | | `pay_url` | string | Ссылка на платёжную страницу Ketmus. Именно её отправляйте покупателю — для любой монеты и для СБП. | | `payment_uri` | string \| null | URI для QR-кода в вашем собственном интерфейсе (`bitcoin:`, `ton:`, `ethereum:` и т. д.). `null`, если у сети нет стандартной схемы URI, например у Tron, — тогда кодируйте в QR сам адрес. Для СБП — прямая ссылка банка `https://qr.nspk.ru/…`: не отправляйте её покупателю вместо `pay_url`. | | `sbp` | object | Только для СБП: `amount_rub` — точная сумма в рублях, `pay_until` — до какого времени действует QR-код. | | `expires_at` | string | Когда истекает счёт (ISO 8601, UTC). | | `paid_at` | string \| null | Когда счёт был оплачен. | --- # Информация о счёте Возвращает счёт с полученными платежами и доступными монетами. `GET /invoices/{id}` Возвращает полную информацию о счёте, включая список платежей `payments` и доступные монеты `available_rails`. ## Параметры пути | Параметр | Тип | Обязательный | Описание | | --- | --- | --- | --- | | `id` | string | да | Идентификатор счёта из поля `invoice.id`. | ## Пример запроса ```bash curl "https://app.ketmus.com/api/merchant/v1/invoices/8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices/8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/invoices/8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Поиск по номеру заказа Чтобы найти счёт по вашему номеру заказа, передайте `order_id` в списке счетов — вернётся один счёт вместо списка: `GET /invoices?order_id=ORDER-1042` ## Ответ ```json { "invoice": { "type": "invoice", "id": "8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d", "order_id": "ORDER-1042", "status": "paid", "is_final": true, "late_payment": false, "price": { "amount": "25", "currency": "USD" }, "payment": { "rail": "usdt_tron", "asset": "USDT", "network": "tron", "address": "TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "memo": null, "amount_due": "25.31", "amount_received": "25.31", "amount_confirmed": "25.31", "late_amount": "0", "rate": "1", "txids": [ "a3f1c0e9d2b84f7c9e61d0b5a2c7e4f8b1d9c3a6e5f2b7d0c4a8e1f6b3d9c2a7" ] }, "fee": { "bps": 125, "payer": "payer", "amount": "0.31", "merchant_amount": "25" }, "pay_url": "https://app.ketmus.com/checkout/inv_5Kq2ZtR8wLmA", "metadata": { "user_id": "u_731" }, "created_at": "2026-11-03T10:15:00.000Z", "expires_at": "2026-11-03T11:15:00.000Z", "paid_at": "2026-11-03T10:21:42.000Z", "public_id": "inv_5Kq2ZtR8wLmA", "description": "Подписка Pro на 1 месяц", "lifetime_sec": 3600, "underpaid_coverage_bps": 0, "refresh_count": 0, "return_url": "https://shop.example.com/orders/1042", "success_url": "https://shop.example.com/orders/1042?paid=1", "fail_url": "https://shop.example.com/orders/1042?failed=1", "payer_email": null, "customer_id": null, "payment_uri": null, "explorer_address_url": "https://tronscan.org/#/address/TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "payments": [ { "txid": "a3f1c0e9d2b84f7c9e61d0b5a2c7e4f8b1d9c3a6e5f2b7d0c4a8e1f6b3d9c2a7", "outputIndex": 0, "address": "TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "amount": "25.31", "confirmations": 20, "requiredConfirmations": 20, "confirmed": true, "late": false, "detectedAt": "2026-11-03T10:19:05.000Z", "explorer_url": "https://tronscan.org/#/transaction/a3f1c0e9d2b84f7c9e61d0b5a2c7e4f8b1d9c3a6e5f2b7d0c4a8e1f6b3d9c2a7" } ] } } ``` --- # Список счетов Возвращает счета магазина с фильтром по статусу и постраничной выдачей. `GET /invoices` ## Параметры запроса | Поле | Тип | Описание | | --- | --- | --- | | `status` | string | Фильтр по статусу, например `paid`. → [Статусы](https://docs.ketmus.com/md/invoice-statuses.md) | | `limit` | integer | Сколько счетов вернуть: от 1 до 200. По умолчанию 50. | | `offset` | integer | Сколько счетов пропустить. По умолчанию 0. | | `order_id` | string | Вернуть один счёт по номеру заказа вместо списка. | ## Пример запроса ```bash curl "https://app.ketmus.com/api/merchant/v1/invoices?status=paid&limit=20&offset=0" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices?status=paid&limit=20&offset=0', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/invoices?status=paid&limit=20&offset=0", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Ответ Счета отсортированы от новых к старым. Чтобы получить следующую страницу, увеличьте `offset` на `limit`. ```json { "invoices": [ { "type": "invoice", "id": "8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d", "order_id": "ORDER-1042", "status": "paid", "is_final": true, "late_payment": false, "price": { "amount": "25", "currency": "USD" }, "payment": { "rail": "usdt_tron", "asset": "USDT", "network": "tron", "address": "TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "memo": null, "amount_due": "25.31", "amount_received": "0", "amount_confirmed": "0", "late_amount": "0", "rate": "1", "txids": [] }, "fee": { "bps": 125, "payer": "payer", "amount": "0", "merchant_amount": "0" }, "pay_url": "https://app.ketmus.com/checkout/inv_5Kq2ZtR8wLmA", "metadata": { "user_id": "u_731" }, "created_at": "2026-11-03T10:15:00.000Z", "expires_at": "2026-11-03T11:15:00.000Z", "paid_at": null, "public_id": "inv_5Kq2ZtR8wLmA", "description": "Подписка Pro на 1 месяц", "lifetime_sec": 3600, "underpaid_coverage_bps": 0, "refresh_count": 0, "return_url": "https://shop.example.com/orders/1042", "success_url": "https://shop.example.com/orders/1042?paid=1", "fail_url": "https://shop.example.com/orders/1042?failed=1", "payer_email": null, "customer_id": null, "payment_uri": null, "explorer_address_url": "https://tronscan.org/#/address/TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt" } ], "limit": 20, "offset": 0 } ``` --- # Отмена счёта Отменяет неоплаченный счёт. `POST /invoices/{id}/cancel` Переводит счёт в статус `cancelled`. Используйте, если клиент отказался от заказа или вы изменили его сумму и создали новый счёт. Оплаченный или уже завершённый счёт отменить нельзя. ## Параметры пути | Параметр | Тип | Обязательный | Описание | | --- | --- | --- | --- | | `id` | string | да | Идентификатор счёта. | ## Пример запроса ```bash curl -X POST "https://app.ketmus.com/api/merchant/v1/invoices/8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d/cancel" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices/8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d/cancel', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.post( "https://app.ketmus.com/api/merchant/v1/invoices/8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d/cancel", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Ответ ```json { "invoice": { "type": "invoice", "id": "8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d", "order_id": "ORDER-1042", "status": "cancelled", "is_final": true, "late_payment": false, "price": { "amount": "25", "currency": "USD" }, "payment": { "rail": "usdt_tron", "asset": "USDT", "network": "tron", "address": "TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "memo": null, "amount_due": "25.31", "amount_received": "0", "amount_confirmed": "0", "late_amount": "0", "rate": "1", "txids": [] }, "fee": { "bps": 125, "payer": "payer", "amount": "0", "merchant_amount": "0" }, "pay_url": "https://app.ketmus.com/checkout/inv_5Kq2ZtR8wLmA", "metadata": { "user_id": "u_731" }, "created_at": "2026-11-03T10:15:00.000Z", "expires_at": "2026-11-03T11:15:00.000Z", "paid_at": null, "public_id": "inv_5Kq2ZtR8wLmA", "description": "Подписка Pro на 1 месяц", "lifetime_sec": 3600, "underpaid_coverage_bps": 0, "refresh_count": 0, "return_url": "https://shop.example.com/orders/1042", "success_url": "https://shop.example.com/orders/1042?paid=1", "fail_url": "https://shop.example.com/orders/1042?failed=1", "payer_email": null, "customer_id": null, "payment_uri": null, "explorer_address_url": "https://tronscan.org/#/address/TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt" } } ``` > Если клиент всё же отправит перевод на адрес отменённого счёта, средства будут зачислены и придёт событие `invoice.late_payment`. --- # Статусы счёта Все статусы счёта, переходы между ними и какие из них окончательные. ## Список статусов | Статус | Окончательный | Описание | | --- | --- | --- | | `new` | Нет | Счёт создан, клиент ещё не выбрал монету | | `pending` | Нет | Монета выбрана, адрес выдан, ожидаем перевод | | `confirming` | Нет | Транзакция найдена в сети, ждём подтверждений | | `wrong_amount_waiting` | Нет | Пришло меньше нужного, ждём доплату до истечения срока | | `paid` | Да | Оплачено полностью — можно выдавать товар | | `paid_over` | Да | Оплачено с переплатой — можно выдавать товар | | `wrong_amount` | Да | Срок истёк, сумма оплачена не полностью | | `expired` | Да | Срок истёк, оплата не поступила | | `cancelled` | Да | Счёт отменён через API или кабинет | ## Типичные переходы ``` new ──► pending ──► confirming ──► paid | paid_over │ │ │ └──► wrong_amount_waiting ──► paid | wrong_amount │ └──► expired new | pending ──► cancelled ``` ## Когда выдавать товар Выдавайте товар при статусе `paid` или `paid_over`. Для `wrong_amount` решите сами: связаться с клиентом, выдать часть товара или вернуть средства через поддержку. --- # Статические адреса Постоянный адрес пополнения для каждого клиента без создания счетов. Статический адрес закрепляется за парой «клиент + монета» и не истекает. Подходит для пополнения баланса в играх, сервисах и ботах: клиент сохраняет адрес и пополняет его в любое время. Каждый подтверждённый перевод становится отдельным оплаченным счётом и вызывает вебхук `static.payment`. ## Получить адрес `POST /static-addresses` | Параметр | Тип | Обязательный | Описание | | --- | --- | --- | --- | | `rail` | string | да | Монета и сеть, например `usdt_tron`. | | `customer_id` | string | да | Ваш идентификатор клиента, до 120 символов. | | `callback_url` | string | нет | URL для вебхуков `static.payment`. По умолчанию — из настроек магазина. | > Метод идемпотентный: повторный вызов с тем же `rail` и `customer_id` вернёт уже выданный адрес, а не создаст новый. ```bash curl -X POST "https://app.ketmus.com/api/merchant/v1/static-addresses" \ -H "Authorization: Bearer $KETMUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "rail": "usdt_tron", "customer_id": "user_731", "callback_url": "https://game.example.com/webhooks/ketmus" }' ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/static-addresses', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ rail: 'usdt_tron', customer_id: 'user_731', callback_url: 'https://game.example.com/webhooks/ketmus', }), }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.post( "https://app.ketmus.com/api/merchant/v1/static-addresses", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, json={ "rail": "usdt_tron", "customer_id": "user_731", "callback_url": "https://game.example.com/webhooks/ketmus", }, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'rail' => 'usdt_tron', 'customer_id' => 'user_731', 'callback_url' => 'https://game.example.com/webhooks/ketmus', ]), ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ### Ответ ```json { "address": { "id": "0b6e2f9a-7c41-4d3e-8a52-19f6c0d4b7e3", "projectId": 42, "rail": "usdt_tron", "customerId": "user_731", "address": "TXa1Pq9vRk3mZcB7dWn4yH2sLf8gUe6oJt", "memo": null, "callbackUrl": "https://game.example.com/webhooks/ketmus", "createdAt": "2026-11-03T09:00:00.000Z", "asset": "USDT", "network": "tron" } } ``` ## Список адресов `GET /static-addresses` ```bash curl "https://app.ketmus.com/api/merchant/v1/static-addresses" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/static-addresses', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/static-addresses", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` Возвращает все статические адреса магазина в поле `addresses` — объекты того же вида, что и при выдаче адреса: ```json { "addresses": [ { "id": "0b6e2f9a-7c41-4d3e-8a52-19f6c0d4b7e3", "projectId": 42, "rail": "usdt_tron", "customerId": "user_731", "address": "TXa1Pq9vRk3mZcB7dWn4yH2sLf8gUe6oJt", "memo": null, "callbackUrl": "https://game.example.com/webhooks/ketmus", "createdAt": "2026-11-03T09:00:00.000Z", "asset": "USDT", "network": "tron" } ] } ``` ## Уведомление о пополнении При каждом подтверждённом переводе приходит событие `static.payment` — с полями счёта и вашим `customer_id`. Зачислите сумму `fee.merchant_amount` или `payment.amount_confirmed` на баланс клиента. --- # Баланс Балансы магазина по каждой монете и количество счетов. `GET /balance` Возвращает доступные балансы магазина в каждой монете и счётчики счетов по статусам. ```bash curl "https://app.ketmus.com/api/merchant/v1/balance" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/balance', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/balance", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Ответ ```json { "balances": [ { "asset": "USDT", "balance": "1520.75" }, { "asset": "BTC", "balance": "0.01834" }, { "asset": "TON", "balance": "312.4" } ], "invoices": { "paid": 351, "paid_over": 4, "pending": 6, "expired": 27, "cancelled": 3 } } ``` `invoices` — количество счетов магазина по статусам. Статусы, по которым счетов нет, в объекте отсутствуют. Вывод средств выполняется в личном кабинете в разделе кошелька. --- # Монеты и сети Список монет и сетей, доступных магазину, с минимальными суммами и подтверждениями. `GET /rails` Возвращает монеты, включённые на платформе, и отмечает, какие из них принимает ваш магазин (`accepted_by_store`). Значение `id` передаётся в параметрах `rail` и `rails` при создании счёта. ```bash curl "https://app.ketmus.com/api/merchant/v1/rails" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/rails', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/rails", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Ответ ```json { "rails": [ { "id": "usdt_tron", "asset": "USDT", "network": "tron", "label": "USDT", "network_label": "Tron (TRC-20)", "decimals": 6, "min_usd": 1, "confirmations": 20, "accepted_by_store": true }, { "id": "btc", "asset": "BTC", "network": "bitcoin", "label": "Bitcoin", "network_label": "Bitcoin", "decimals": 8, "min_usd": 5, "confirmations": 2, "accepted_by_store": true } ], "fee": { "bps": 125, "payer": "payer" } } ``` Полный справочник монет — в разделе [Поддерживаемые монеты](https://docs.ketmus.com/md/coins.md). --- # Курсы Актуальные курсы криптовалют к USD и фиатным валютам. `GET /rates` Возвращает курсы, по которым Ketmus пересчитывает цену счёта в сумму к оплате. Удобно, чтобы заранее показать клиенту примерную стоимость в криптовалюте. ```bash curl "https://app.ketmus.com/api/merchant/v1/rates" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/rates', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/rates", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Ответ ```json { "usd": { "BTC": 68250.12, "ETH": 2480.5, "USDT": 1, "TON": 5.12, "SOL": 162.3 }, "fiat": { "USD": 1, "USDT": 1, "EUR": 0.92, "RUB": 92.4, "UAH": 41.2, "KZT": 478.6, "TRY": 34.1, "GBP": 0.78 }, "fetched_at": "2026-11-03T10:14:30.000Z" } ``` - `usd` — цена одной монеты в долларах США. - `fiat` — сколько единиц валюты даёт 1 USD. Например, `RUB: 92.4` означает, что 1 USD = 92,4 ₽. - `fetched_at` — когда курсы были обновлены (ISO 8601, UTC). Курсы кешируются примерно на минуту, поэтому не опрашивайте метод чаще. > Точная сумма к оплате фиксируется в момент выбора монеты и возвращается в `payment.amount_due` и `payment.rate` счёта. --- # Информация о магазине Настройки магазина, к которому относится API-ключ. `GET /me` Возвращает магазин, к которому привязан ключ, и его настройки по умолчанию. Удобно для проверки ключа при запуске приложения. ```bash curl "https://app.ketmus.com/api/merchant/v1/me" \ -H "Authorization: Bearer $KETMUS_API_KEY" ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/me', { headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, }, }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.get( "https://app.ketmus.com/api/merchant/v1/me", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), ], ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Ответ ```json { "store": { "id": 42, "name": "My Shop" }, "accepted_rails": [ "usdt_tron", "usdt_ton", "btc", "eth", "ton" ], "webhook_url": "https://shop.example.com/webhooks/ketmus", "fee_payer": "payer", "lifetime_sec": 3600 } ``` --- # Вебхуки Уведомления об изменении статуса счёта, которые Ketmus отправляет на ваш сервер. При каждом изменении статуса счёта Ketmus отправляет POST-запрос с JSON на `callback_url` счёта или, если он не задан, на URL из настроек магазина. Запрос подписан секретом вебхуков — [проверяйте подпись](https://docs.ketmus.com/md/webhook-signature.md) перед обработкой. ## События | Событие | Когда приходит | | --- | --- | | `invoice.pending` | Клиент выбрал монету, адрес выдан | | `invoice.confirming` | Транзакция найдена в сети, ожидаются подтверждения | | `invoice.paid` | Счёт оплачен полностью | | `invoice.paid_over` | Счёт оплачен с переплатой | | `invoice.wrong_amount_waiting` | Пришла неполная сумма, счёт ждёт доплаты | | `invoice.wrong_amount` | Счёт истёк с неполной оплатой | | `invoice.expired` | Счёт истёк без оплаты | | `invoice.cancelled` | Счёт отменён | | `invoice.late_payment` | Платёж пришёл после истечения или отмены счёта | | `static.payment` | Пополнение [статического адреса](https://docs.ketmus.com/md/static-addresses.md) | | `webhook.test` | Тестовое событие из личного кабинета | ## Заголовки запроса | Заголовок | Пример | Описание | | --- | --- | --- | | `content-type` | `application/json` | Тело — JSON | | `user-agent` | `ketmus-webhooks/2.0` | Отправитель | | `x-ketmus-event` | `invoice.paid` | Тип события | | `x-ketmus-delivery` | `5d2b8e61-0f4a-…` | Уникальный ID доставки, совпадает с `delivery_id` в теле | | `x-ketmus-timestamp` | `1793787702` | Время отправки, Unix-секунды | | `x-ketmus-signature` | `t=1793787702,v1=9f3c...` | Подпись HMAC-SHA512. → [Проверка подписи](https://docs.ketmus.com/md/webhook-signature.md) | ## Тело уведомления Тело содержит актуальное состояние счёта, а также поля `event` и `delivery_id`: ```json { "type": "invoice", "id": "8f0c6a52-3f7e-4b8e-9d61-2c4b0f8a1e7d", "order_id": "ORDER-1042", "status": "paid", "is_final": true, "late_payment": false, "price": { "amount": "25", "currency": "USD" }, "payment": { "rail": "usdt_tron", "asset": "USDT", "network": "tron", "address": "TQ7nX4yVh3cE9v5G2kRZb8aWm1pLd6sFJt", "memo": null, "amount_due": "25.31", "amount_received": "25.31", "amount_confirmed": "25.31", "late_amount": "0", "rate": "1", "txids": [ "a3f1c0e9d2b84f7c9e61d0b5a2c7e4f8b1d9c3a6e5f2b7d0c4a8e1f6b3d9c2a7" ] }, "fee": { "bps": 125, "payer": "payer", "amount": "0.31", "merchant_amount": "25" }, "pay_url": "https://app.ketmus.com/checkout/inv_5Kq2ZtR8wLmA", "metadata": { "user_id": "u_731" }, "created_at": "2026-11-03T10:15:00.000Z", "expires_at": "2026-11-03T11:15:00.000Z", "paid_at": "2026-11-03T10:21:42.000Z", "event": "invoice.paid", "delivery_id": "5d2b8e61-0f4a-4c9b-a7e3-6b1d9f2c8a40" } ``` Для события `static.payment` в теле дополнительно передаётся `customer_id` клиента, которому принадлежит адрес. ## Как отвечать - Верните любой код **2xx** в течение 10 секунд — доставка будет считаться успешной. - Тяжёлую обработку (выдачу товара, письма) выполняйте асинхронно, после ответа. - Редиректы не выполняются: ответ 3xx считается ошибкой. Указывайте окончательный HTTPS-адрес. ## Повторная отправка Если ваш сервер не ответил 2xx, Ketmus повторит отправку по расписанию: через 5 минут, 30 минут, 2 часа, 6 часов и 24 часа — всего до 6 попыток. История доставок и кнопка повторной отправки доступны в личном кабинете. ## Идемпотентность и порядок - Одно событие может прийти несколько раз — сохраняйте `delivery_id` и пропускайте уже обработанные. - События могут прийти не по порядку. Ориентируйтесь на `status` и `is_final` в теле, а не на порядок получения. - Если сомневаетесь, запросите актуальное состояние: `GET /invoices/{id}`. --- # Проверка подписи Как убедиться, что вебхук отправлен Ketmus и не был изменён. ## Алгоритм 1. **Получите сырое тело.** Возьмите тело запроса в исходном виде — до разбора JSON. Любое переформатирование сломает подпись. 2. **Разберите заголовок.** Из `x-ketmus-signature` вида `t=,v1=` возьмите `t` и `v1`. 3. **Вычислите HMAC.** Посчитайте HMAC-SHA512 от строки `.<сырое тело>` секретом вебхуков магазина, результат — в hex. 4. **Сравните.** Сравните результат с `v1` функцией постоянного времени и проверьте, что `t` не старше 5 минут. ## Примеры ```javascript import crypto from 'node:crypto' /** * rawBody — тело запроса в исходном виде (строка или Buffer), до JSON.parse. * header — значение заголовка x-ketmus-signature. */ export function verifyKetmusSignature(rawBody, header, secret, toleranceSec = 300) { const parts = Object.fromEntries(header.split(',').map((part) => part.trim().split('='))) const timestamp = Number(parts.t) if (!timestamp || !parts.v1) return false if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false const expected = crypto.createHmac('sha512', secret).update(`${timestamp}.${rawBody}`).digest('hex') const a = Buffer.from(expected) const b = Buffer.from(parts.v1) return a.length === b.length && crypto.timingSafeEqual(a, b) } ``` ```python import hashlib import hmac import time def verify_ketmus_signature(raw_body: bytes, header: str, secret: str, tolerance_sec: int = 300) -> bool: parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part) try: timestamp = int(parts["t"]) received = parts["v1"] except (KeyError, ValueError): return False if abs(time.time() - timestamp) > tolerance_sec: return False signed = f"{timestamp}.".encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha512).hexdigest() return hmac.compare_digest(expected, received) ``` ```php $toleranceSec) return false; $expected = hash_hmac('sha512', $parts['t'] . '.' . $rawBody, $secret); return hash_equals($expected, $parts['v1']); } $rawBody = file_get_contents('php://input'); $header = $_SERVER['HTTP_X_KETMUS_SIGNATURE'] ?? ''; if (!verifyKetmusSignature($rawBody, $header, getenv('KETMUS_WEBHOOK_SECRET'))) { http_response_code(400); exit('bad signature'); } $event = json_decode($rawBody, true); // ... обработка события http_response_code(200); ``` ## Сырое тело во фреймворках | Фреймворк | Как получить сырое тело | | --- | --- | | Express | `express.raw({ type: 'application/json' })` на маршруте вебхука, тело — в `req.body` | | Next.js (App Router) | `await request.text()` в Route Handler | | FastAPI | `await request.body()` | | Django | `request.body` | | Laravel | `$request->getContent()` | | PHP | `file_get_contents('php://input')` | > **Не пропускайте проверку.** Без проверки подписи злоумышленник может отправить на ваш URL поддельное событие `invoice.paid` и получить товар бесплатно. --- # Сайт и интернет-магазин Подключение оплаты криптовалютой к сайту: кнопка оплаты, платёжная страница и выдача заказа. ## Схема 1. **Покупатель нажимает «Оплатить».** Ваш фронтенд отправляет запрос на ваш сервер с номером заказа. 2. **Сервер создаёт счёт.** Сервер берёт сумму заказа из своей базы и вызывает `POST /invoices`. 3. **Редирект на оплату.** Покупатель уходит на `pay_url`, выбирает монету и оплачивает. 4. **Вебхук и выдача заказа.** По событию `invoice.paid` сервер отмечает заказ оплаченным, а покупатель возвращается на `success_url`. > **Сумму определяет только сервер.** Никогда не принимайте сумму счёта из браузера. Берите её из своей базы по номеру заказа, иначе покупатель сможет изменить цену. ## Пример на Next.js ```typescript // app/api/checkout/route.ts — создаёт счёт и возвращает ссылку на оплату export async function POST(request: Request) { const { orderId } = await request.json() const order = await db.orders.find(orderId) // сумму берите только из своей базы const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: order.total, currency: 'USD', order_id: order.id, description: `Заказ №${order.number}`, callback_url: 'https://shop.example.com/api/webhooks/ketmus', success_url: `https://shop.example.com/orders/${order.id}?paid=1`, fail_url: `https://shop.example.com/orders/${order.id}`, }), }) const data = await response.json() if (!response.ok) return Response.json({ error: data.error.message }, { status: 502 }) return Response.json({ url: data.invoice.pay_url }) } ``` ```typescript // app/api/webhooks/ketmus/route.ts import { verifyKetmusSignature } from '@/lib/ketmus' export async function POST(request: Request) { const rawBody = await request.text() const signature = request.headers.get('x-ketmus-signature') ?? '' if (!verifyKetmusSignature(rawBody, signature, process.env.KETMUS_WEBHOOK_SECRET!)) { return new Response('bad signature', { status: 400 }) } const event = JSON.parse(rawBody) if (await db.webhookDeliveries.exists(event.delivery_id)) return new Response('ok') if (event.event === 'invoice.paid' || event.event === 'invoice.paid_over') { await db.orders.markPaid(event.order_id, { invoiceId: event.id }) } await db.webhookDeliveries.save(event.delivery_id) return new Response('ok') } ``` ## Пример на PHP ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => $order['total'], 'currency' => 'USD', 'order_id' => (string) $order['id'], 'callback_url' => 'https://shop.example.com/ketmus-webhook.php', 'success_url' => 'https://shop.example.com/order.php?id=' . $order['id'], ]), ]); $data = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($status !== 201 || empty($data['invoice']['pay_url'])) { error_log('Ketmus error: ' . json_encode($data['error'] ?? $data)); http_response_code(502); exit('Не удалось создать счёт, попробуйте позже.'); } header('Location: ' . $data['invoice']['pay_url'], true, 303); ``` Обработчик вебхука на PHP — в разделе [Проверка подписи](https://docs.ketmus.com/md/webhook-signature.md). ## Оплата в своём интерфейсе Если не хотите уводить покупателя на страницу Ketmus, передайте `rail` при создании счёта. В ответе сразу будут `payment.address`, `payment.amount_due` и `payment_uri` — покажите их вместе с QR-кодом из `payment_uri`, а статус узнавайте через вебхуки. ```bash curl -X POST "https://app.ketmus.com/api/merchant/v1/invoices" \ -H "Authorization: Bearer $KETMUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 49, "currency": "EUR", "order_id": "ORDER-2077", "rail": "usdt_ton" }' ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 49, currency: 'EUR', order_id: 'ORDER-2077', rail: 'usdt_ton', }), }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.post( "https://app.ketmus.com/api/merchant/v1/invoices", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, json={ "amount": 49, "currency": "EUR", "order_id": "ORDER-2077", "rail": "usdt_ton", }, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => 49, 'currency' => 'EUR', 'order_id' => 'ORDER-2077', 'rail' => 'usdt_ton', ]), ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` > Для монет с `memo` (например, XRP) обязательно покажите memo рядом с адресом — без него платёж не будет привязан к счёту. > СБП (`sbp_qr`) всегда оплачивается через `pay_url`. Прямую ссылку `https://qr.nspk.ru/…` из `payment_uri` покупателю не отправляйте — подробнее в разделе [Оплата по СБП](https://docs.ketmus.com/md/coins.md#sbp). --- # Telegram-бот Приём криптоплатежей в Telegram-боте и Mini App: подписки, доступ к каналам и цифровые товары. Боту не нужен сайт: счёт создаёт бэкенд бота, клиент оплачивает по кнопке со ссылкой, а Ketmus присылает вебхук, по которому бот выдаёт доступ и пишет пользователю. ## Схема 1. **Пользователь отправляет /buy.** Бот создаёт счёт и кладёт `telegram_user_id` в `metadata`. 2. **Бот присылает кнопку.** Inline-кнопка с `pay_url` открывает платёжную страницу во встроенном браузере Telegram. 3. **Пользователь оплачивает.** Удобнее всего — в TON или USDT в сети TON через Telegram Wallet или Tonkeeper. 4. **Бот получает вебхук.** По `invoice.paid` бот выдаёт доступ и отправляет сообщение пользователю из `metadata.telegram_user_id`. ## Пример на Python (aiogram) ```python # bot.py — aiogram 3 import os import aiohttp from aiogram import Bot, Dispatcher, F from aiogram.filters import Command from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, Message API = "https://app.ketmus.com/api/merchant/v1" HEADERS = {"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"} bot = Bot(os.environ["BOT_TOKEN"]) dp = Dispatcher() @dp.message(Command("buy")) async def buy(message: Message): payload = { "amount": 9.99, "currency": "USD", "order_id": f"tg-{message.from_user.id}-{message.message_id}", "description": "Подписка Premium на 30 дней", "metadata": {"telegram_user_id": message.from_user.id, "plan": "premium_30d"}, "callback_url": "https://bot.example.com/webhooks/ketmus", "return_url": "https://t.me/your_bot", } async with aiohttp.ClientSession(headers=HEADERS) as session: async with session.post(f"{API}/invoices", json=payload) as response: data = await response.json() if "invoice" not in data: await message.answer("Не удалось создать счёт, попробуйте позже.") print("Ketmus error:", data.get("error")) return invoice = data["invoice"] keyboard = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Оплатить криптовалютой", url=invoice["pay_url"])]]) await message.answer("Счёт на 9.99 USD создан. Оплатите в течение часа:", reply_markup=keyboard) ``` ```python # webhook.py — aiohttp-сервер рядом с ботом import os from aiohttp import web from bot import bot from signature import verify_ketmus_signature # см. «Проверка подписи» async def ketmus_webhook(request: web.Request): raw = await request.read() if not verify_ketmus_signature(raw, request.headers.get("x-ketmus-signature", ""), os.environ["KETMUS_WEBHOOK_SECRET"]): return web.Response(status=400, text="bad signature") event = await request.json() # webhook.test и static.payment приходят без metadata счёта user_id = (event.get("metadata") or {}).get("telegram_user_id") if not user_id: return web.Response(text="ok") # Выдаём доступ один раз на счёт: id счёта — ключ идемпотентности if event["event"] in ("invoice.paid", "invoice.paid_over") and await mark_processed_once(event["id"]): await grant_premium(user_id, days=30) await bot.send_message(user_id, "Оплата получена, Premium активирован.") elif event["event"] == "invoice.expired": await bot.send_message(user_id, "Счёт истёк. Отправьте /buy, чтобы создать новый.") return web.Response(text="ok") app = web.Application() app.router.add_post("/webhooks/ketmus", ketmus_webhook) ``` ## Пример на Node.js (grammY) ```typescript // bot.ts — grammY import { Bot, InlineKeyboard } from 'grammy' const bot = new Bot(process.env.BOT_TOKEN!) bot.command('buy', async (ctx) => { const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 9.99, currency: 'USD', order_id: `tg-${ctx.from!.id}-${ctx.msg.message_id}`, description: 'Подписка Premium на 30 дней', metadata: { telegram_user_id: ctx.from!.id, plan: 'premium_30d' }, rails: ['usdt_ton', 'ton', 'usdt_tron'], callback_url: 'https://bot.example.com/webhooks/ketmus', }), }) const data = await response.json() if (!response.ok) { console.error('Ketmus error:', data.error) return ctx.reply('Не удалось создать счёт, попробуйте позже.') } const { invoice } = data await ctx.reply('Счёт на 9.99 USD создан. Оплатите в течение часа:', { reply_markup: new InlineKeyboard().url('Оплатить криптовалютой', invoice.pay_url), }) }) bot.start() ``` ## Telegram Mini App В Mini App счёт тоже создаёт ваш сервер: передайте ему `initData`, проверьте подпись Telegram, создайте счёт и откройте `pay_url` через `Telegram.WebApp.openLink`. ```javascript // Внутри Telegram Mini App: счёт создаёт ваш сервер, а не клиент const { url } = await fetch('/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Telegram-Init-Data': Telegram.WebApp.initData }, body: JSON.stringify({ plan: 'premium_30d' }), }).then((r) => r.json()) Telegram.WebApp.openLink(url) // платёжная страница откроется во встроенном браузере ``` ## Советы - Ограничьте монеты параметром `rails`, например `["usdt_ton", "ton", "usdt_tron"]` — для аудитории Telegram это самые быстрые и дешёвые сети. - Кнопка «Оплатить» всегда ведёт на `pay_url`, в том числе для СБП. Не ставьте на неё ссылку `qr.nspk.ru` из `payment_uri`: на странице Ketmus покупатель видит, через кого платит, видит сумму, таймер и статус, а потом возвращается в бота. → [Оплата по СБП](https://docs.ketmus.com/md/coins.md#sbp) - Для пополнения баланса в боте используйте [статические адреса](https://docs.ketmus.com/md/static-addresses.md) с `customer_id` = Telegram ID пользователя. - Передайте `return_url: "https://t.me/your_bot"`, чтобы после оплаты пользователь вернулся в бота. - API-ключ хранится только на сервере бота — не встраивайте его в Mini App. --- # Для бизнеса Выставление счетов клиентам и партнёрам, ссылки на оплату и сервисы без разработки. Не каждому бизнесу нужна интеграция по API. Ketmus позволяет принимать оплату в криптовалюте прямо из личного кабинета — для услуг, B2B-расчётов, фриланса и донатов. ## Без разработки - **Ссылка на оплату.** Создайте ссылку с суммой и описанием в кабинете и отправьте клиенту в мессенджере или по почте. - **Счёт из кабинета.** Выставьте счёт с номером заказа и сроком оплаты, следите за статусом в списке счетов. - **Сервис донатов.** Страница для пожертвований и поддержки автора без своего сайта. ## Счета для B2B по API Для расчётов с партнёрами создавайте счета с максимальным сроком жизни (24 часа), фиксированной валютой цены и email плательщика. Номер договора или акта передайте в `order_id`, реквизиты — в `metadata`. ```bash curl -X POST "https://app.ketmus.com/api/merchant/v1/invoices" \ -H "Authorization: Bearer $KETMUS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "amount": 4800, "currency": "EUR", "order_id": "ACT-2026-118", "description": "Разработка, этап 2 по договору №17/2026", "rails": [ "usdt_tron", "usdt_eth", "usdc_eth", "btc" ], "lifetime_sec": 86400, "fee_payer": "merchant", "payer_email": "finance@partner.example", "metadata": { "contract": "17/2026", "company": "Partner LLC" } }' ``` ```javascript const response = await fetch('https://app.ketmus.com/api/merchant/v1/invoices', { method: 'POST', headers: { Authorization: `Bearer ${process.env.KETMUS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: 4800, currency: 'EUR', order_id: 'ACT-2026-118', description: 'Разработка, этап 2 по договору №17/2026', rails: ['usdt_tron', 'usdt_eth', 'usdc_eth', 'btc'], lifetime_sec: 86400, fee_payer: 'merchant', payer_email: 'finance@partner.example', metadata: { contract: '17/2026', company: 'Partner LLC', }, }), }) const data = await response.json() if (!response.ok) throw new Error(data.error.message) ``` ```python import os import requests response = requests.post( "https://app.ketmus.com/api/merchant/v1/invoices", headers={"Authorization": f"Bearer {os.environ['KETMUS_API_KEY']}"}, json={ "amount": 4800, "currency": "EUR", "order_id": "ACT-2026-118", "description": "Разработка, этап 2 по договору №17/2026", "rails": ["usdt_tron", "usdt_eth", "usdc_eth", "btc"], "lifetime_sec": 86400, "fee_payer": "merchant", "payer_email": "finance@partner.example", "metadata": { "contract": "17/2026", "company": "Partner LLC", }, }, timeout=30, ) data = response.json() response.raise_for_status() ``` ```php true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('KETMUS_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'amount' => 4800, 'currency' => 'EUR', 'order_id' => 'ACT-2026-118', 'description' => 'Разработка, этап 2 по договору №17/2026', 'rails' => ['usdt_tron', 'usdt_eth', 'usdc_eth', 'btc'], 'lifetime_sec' => 86400, 'fee_payer' => 'merchant', 'payer_email' => 'finance@partner.example', 'metadata' => [ 'contract' => '17/2026', 'company' => 'Partner LLC', ], ]), ]); $data = json_decode(curl_exec($ch), true); curl_close($ch); ``` ## Учёт и сверка - Выгружайте оплаченные счета через `GET /invoices?status=paid` с постраничной выдачей. - Для каждого счёта сохраняйте `payment.txids` — это подтверждение оплаты в блокчейне. - Сумма зачисления магазину после комиссии — в поле `fee.merchant_amount`. --- # Платёжная страница Что видит клиент на странице оплаты Ketmus и как настроить возврат на ваш сайт. Платёжная страница по адресу `pay_url` — готовый интерфейс оплаты: выбор монеты и сети, адрес с QR-кодом, точная сумма, таймер и статус платежа в реальном времени. Если магазину включена оплата по СБП, здесь же клиент увидит QR-код и кнопку перехода в приложение банка. Страница адаптирована под мобильные устройства и встроенный браузер Telegram. ## Возврат на ваш сайт | Параметр | Когда используется | | --- | --- | | `success_url` | Автоматический переход после успешной оплаты | | `fail_url` | Переход, если счёт истёк или отменён | | `return_url` | Кнопка «Вернуться в магазин» на странице оплаты | Если параметры не переданы в запросе, используются значения из настроек магазина. ## Как открывать страницу - Перенаправьте клиента (HTTP 303) или откройте `pay_url` в новой вкладке. - Встраивание в `iframe` не поддерживается — из соображений безопасности страница оплаты не открывается внутри чужих сайтов. - В Telegram используйте inline-кнопку с URL или `Telegram.WebApp.openLink`. ## Выбор монет По умолчанию клиент видит все монеты, включённые в настройках магазина. Чтобы показать только часть, передайте `rails`, а чтобы сразу открыть оплату в конкретной монете — `rail`. --- # Поддерживаемые монеты Идентификаторы монет и сетей, число подтверждений и минимальные суммы счёта. Идентификатор из колонки `rail` передаётся в параметрах `rail` и `rails` при создании счёта. Набор монет, доступных именно вашему магазину, возвращает метод [GET /rails](https://docs.ketmus.com/md/rails.md) — значения подтверждений там могут отличаться, если их изменили на платформе. ## Список монет | rail | Монета | Сеть | Подтверждений | Мин. счёт, USD | | --- | --- | --- | --- | --- | | `usdt_tron` | USDT | Tron (TRC-20) | 20 | 1 | | `usdt_eth` | USDT | Ethereum (ERC-20) | 12 | 10 | | `usdt_bsc` | USDT | BNB Smart Chain (BEP-20) | 30 | 1 | | `usdt_pol` | USDT | Polygon PoS | 64 | 1 | | `usdt_ton` | USDT | TON (jetton) | 1 | 1 | | `usdt_sol` | USDT | Solana (SPL) | 32 | 1 | | `usdc_eth` | USDC | Ethereum (ERC-20) | 12 | 10 | | `usdc_bsc` | USDC | BNB Smart Chain (BEP-20) | 30 | 1 | | `usdc_pol` | USDC | Polygon PoS | 64 | 1 | | `dai_eth` | DAI | Ethereum (ERC-20) | 12 | 10 | | `dai_bsc` | DAI | BNB Smart Chain (BEP-20) | 30 | 1 | | `dai_pol` | DAI | Polygon PoS | 64 | 1 | | `btc` | BTC | Bitcoin | 2 | 5 | | `ltc` | LTC | Litecoin | 6 | 2 | | `doge` | DOGE | Dogecoin | 20 | 2 | | `bch` | BCH | Bitcoin Cash | 6 | 2 | | `eth` | ETH | Ethereum | 12 | 10 | | `eth_base` | ETH | Base | 60 | 1 | | `eth_arb` | ETH | Arbitrum One | 64 | 1 | | `eth_op` | ETH | OP Mainnet | 60 | 1 | | `eth_zksync` | ETH | ZKsync Era | 60 | 1 | | `eth_unichain` | ETH | Unichain | 64 | 1 | | `bnb` | BNB | BNB Smart Chain | 30 | 2 | | `pol` | POL | Polygon PoS | 64 | 1 | | `avax` | AVAX | Avalanche C-Chain | 4 | 1 | | `sol` | SOL | Solana | 32 | 1 | | `ton` | TON | TON | 1 | 1 | | `not_ton` | NOT | TON (jetton) | 1 | 1 | | `dogs_ton` | DOGS | TON (jetton) | 1 | 1 | | `trx` | TRX | Tron | 20 | 1 | | `xrp` | XRP | XRP Ledger | 1 | 1 | | `xmr` | XMR | Monero | 10 | 1 | | `shib_eth` | SHIB | Ethereum (ERC-20) | 12 | 10 | | `sbp_qr` | Рубли → USDT | СБП QR — [подробнее](#sbp) | 1 | от 10 ₽ | ## Валюты цены Цену счёта можно задать в `USD`, `USDT`, `EUR`, `RUB`, `UAH`, `KZT`, `TRY` или `GBP` — Ketmus пересчитает её в выбранную клиентом монету по актуальному курсу. ## Оплата по СБП Способ `sbp_qr` — оплата рублями через приложение банка по Системе быстрых платежей, от 10 ₽. Магазину сумма зачисляется в USDT. Точная сумма в рублях и срок действия QR-кода приходят в поле `sbp` объекта счёта. > **Отправляйте покупателя на pay_url.** Для СБП `payment_uri` и `payment.address` содержат прямую ссылку банка `https://qr.nspk.ru/…`. Не ставьте её на кнопку «Оплатить»: покупатель не увидит, что платит через Ketmus, не увидит таймер и статус оплаты и не вернётся в магазин. На странице `pay_url` есть QR-код и кнопка перехода в приложение банка, так что на телефоне оплата так же удобна. ## Монеты с memo Для XRP все счета используют общий адрес платформы, а платёж определяется по destination tag из `payment.memo`. Обязательно показывайте его клиенту. --- # Поддержка и FAQ Ответы на частые вопросы об интеграции Ketmus. ## Клиент отправил монету не в той сети Например, USDT в сети BEP-20 на адрес TRC-20. Такой перевод не будет зачислен автоматически — напишите в поддержку с TXID и номером счёта. ## Не приходят вебхуки - Проверьте, что URL доступен из интернета по HTTPS и не требует авторизации. - Убедитесь, что сервер отвечает 2xx без редиректа в течение 10 секунд. - Посмотрите историю доставок в кабинете — там видны код ответа и текст ошибки. - Отправьте тестовый вебхук из настроек магазина. ## Подпись не совпадает Чаще всего фреймворк разбирает JSON до проверки, и вы подписываете уже изменённое тело. Используйте сырое тело запроса — см. [Проверка подписи](https://docs.ketmus.com/md/webhook-signature.md). Также проверьте, что используете секрет вебхуков, а не API-ключ. ## Клиент заплатил меньше Счёт перейдёт в `wrong_amount_waiting` и будет ждать доплату на тот же адрес до истечения срока. Чтобы не терять заказы из-за копеечной разницы, задайте `underpaid_coverage_bps`. ## Как вернуть платёж Криптовалютные переводы необратимы. Для возврата отправьте средства клиенту с баланса через вывод в личном кабинете или обратитесь в поддержку. ## Связаться с нами Чат поддержки доступен в [личном кабинете](https://app.ketmus.com) и на [сайте Ketmus](https://ketmus.com). Укажите ID счёта, TXID и время запроса — так мы ответим быстрее.