# TabPay API - полная документация для LLM > TabPay - приём платежей в РФ: СБП и банковские карты (3-D Secure). > REST API: мерчант создаёт платёж, отправляет покупателя на платёжную > страницу TabPay, результат получает подписанным вебхуком. > Человеческая версия документации: https://tabpay.org/docs ## База - Базовый адрес: `https://tabpay.org/api` - Формат: JSON, UTF-8. - ДЕНЬГИ ВСЕГДА В КОПЕЙКАХ, целыми числами: 19900 = 199,00 RUB. Дробных рублей нет. - Даты: строки ISO 8601 в UTC. - Песочницы нет, API боевое: интеграцию проверяют платежом на небольшую сумму. - Машиночитаемая спецификация: https://tabpay.org/openapi.json ## Аутентификация - Каждый запрос - заголовок `X-Api-Key: tp_...` (API-ключ магазина). - Ключ выпускается в личном кабинете в карточке активного магазина, показывается один раз; перевыпуск отзывает старый. - Любой сбой авторизации - единый ответ 401 без уточнения причины. - Ключ используют только на сервере: не в браузере, не в мобильном приложении, не в репозитории. - Рекомендованные имена переменных окружения: `TABPAY_API_KEY` (ключ API), `TABPAY_WEBHOOK_SECRET` (секрет подписи вебхуков). ## Создание платежа POST /api/v1/payments Тело (JSON): | Поле | Тип | Обязательное | Описание | |---|---|---|---| | orderId | string | да | Номер заказа мерчанта, 1-64 символа. Уникален в рамках магазина: повтор - 409 (платежи не задваиваются). | | amountKopecks | integer | да | Сумма в копейках. Минимум 100 (1 рубль), максимум 10000000000 (100 млн рублей). | | description | string | нет | Назначение платежа, до 255 символов; видно покупателю. | | email | string | нет | Email покупателя. | | telegramId | integer или string | нет | Телеграм-ID покупателя (для ботов). Принимается числом или строкой цифр (1-20), возвращается строкой в объекте платежа и в вебхуке. Провайдеру не уходит. | | metadata | object | нет | Произвольные данные (объект JSON, до 4 КБ). Провайдеру не уходят, возвращаются эхом в объекте платежа и в вебхуке. Для id товара, тарифа, id сообщения - без своей таблицы соответствий. | | successUrl | string | нет | Ссылка возврата после успешной оплаты (https, t.me, tg://). Переопределяет магазинную; не задана - магазинная. | | failUrl | string | нет | Ссылка возврата после неудачной оплаты. Правила как у successUrl. | | method | string | нет | "SBP" или "CARD" - зафиксировать способ. Не задан - покупатель выберет сам. Способ должен быть включён магазину, иначе 409. | Пример запроса: ```bash curl -X POST https://tabpay.org/api/v1/payments \ -H "X-Api-Key: tp_ваш_ключ" \ -H "Content-Type: application/json" \ -d '{"orderId": "order-1001", "amountKopecks": 19900, "description": "Подписка на месяц"}' ``` Ответ 201 - объект платежа: ```json { "id": "6b9d2c88-4b1a-4f0e-9c37-1f2ab34cd561", "orderId": "order-1001", "status": "CREATED", "amountKopecks": 19900, "commissionKopecks": 1393, "description": "Подписка на месяц", "method": null, "telegramId": null, "metadata": null, "successUrl": null, "failUrl": null, "payUrl": "https://tabpay.org/pay/6b9d2c88-4b1a-4f0e-9c37-1f2ab34cd561", "paidAt": null, "createdAt": "2026-07-11T10:20:30.000Z" } ``` Дальше: отправить покупателя на `payUrl` (редирект или ссылка сообщением). Ссылка не сгорает, пока покупатель не начал оплату; после начала оплаты даётся 20 минут. Если ответ на создание не получен (сеть, 5xx) - НЕ создавать повторно вслепую: сначала GET по своему orderId (см. ниже); 404 - создавать заново, найден - использовать его payUrl. Повтор с тем же orderId безопасен: придёт 409. ## Получение платежа - GET /api/v1/payments/{id} - по идентификатору TabPay. - GET /api/v1/payments?orderId={orderId} - по номеру заказа мерчанта. Ответ 200 - объект платежа (формат как при создании). Чужой или несуществующий платёж - 404. Для регулярного получения результатов использовать вебхуки, а не опрос в цикле. ## Список платежей (сверка и отчётность) GET /api/v1/payments (без orderId) - страница платежей магазина. Параметры: `from`, `to` - границы периода по createdAt (ISO 8601, UTC, включительно; from позже to - 400); `status` - фильтр статусов через запятую (например SUCCESS,REFUNDED); `page` (1..10000, по умолчанию 1); `limit` (1..100, по умолчанию 20). Ответ 200: `{"items": [объекты платежа], "total": N, "page": 1, "pageSize": 20}`. Внутри страницы платежи от старых к новым; страницы стабильны - новые платежи дописываются в конец. ВАЖНО для сверки: период фильтрует по времени СОЗДАНИЯ (createdAt), момент оплаты - поле paidAt; нефинальные статусы могут позже измениться, SUCCESS может стать REFUNDED - закрытые периоды при расследовании расхождений перечитывать. С параметром orderId это ПРЕЖНИЙ поиск одного платежа: возвращается объект, не массив. ## Отмена платежа POST /api/v1/payments/{id}/cancel - отмена платежа ДО начала оплаты (только статус CREATED). Ответ 200 - объект платежа в статусе CANCELED; ссылка payUrl сразу показывает покупателю экран «Счёт отменён», магазину уходит вебхук со статусом CANCELED. - Повторная отмена идемпотентна: снова 200, без второго вебхука. - Покупатель уже начал оплату (PENDING) или платёж финален - 409; оплата ещё может пройти, дождитесь финального вебхука. - orderId отменённого платежа остаётся занят: для новой попытки создать платёж с НОВЫМ orderId (например, с суффиксом попытки). ## Баланс GET /api/v1/balance - баланс мерчанта, те же цифры, что в кабинете. Ответ 200: `{"availableKopecks": 1250000, "frozenKopecks": 89000, "holdHour": 12}`. - availableKopecks - доступно к выводу; frozenKopecks - заморожено. - Поступления дня D замораживаются до holdHour (по Москве) дня D+1. - Баланс считается по ВСЕМУ аккаунту владельца магазина (все его магазины и выплаты вместе): ключ любого магазина аккаунта вернёт одну сумму. - Лимит этой ручки - 60 запросов в минуту по ключу; опрашивать чаще раза в минуту нет смысла. ## Объект платежа (поля) | Поле | Тип | Описание | |---|---|---| | id | string (UUID) | Идентификатор платежа в TabPay. | | orderId | string | Номер заказа мерчанта. | | status | string | См. статусы ниже. | | amountKopecks | integer | Сумма в копейках. | | commissionKopecks | integer | Комиссия TabPay в копейках. | | description | string или null | Назначение платежа. | | method | string или null | "SBP", "CARD" или null до выбора покупателем. | | telegramId | string или null | Телеграм-ID покупателя из запроса на создание. | | metadata | object или null | Произвольные данные мерчанта (эхо) или null. | | successUrl | string или null | Ссылка возврата уровня платежа после успеха; null - магазинная. | | failUrl | string или null | Ссылка возврата уровня платежа после неудачи; null - магазинная. | | payUrl | string | Ссылка на платёжную страницу. | | isTest | boolean | Тестовый платёж (магазин-песочница): исход задаётся на платёжной странице, деньги не двигаются, вебхук с test: true. | | paidAt | string или null | Момент оплаты, ISO 8601. | | createdAt | string | Момент создания, ISO 8601. | ## Статусы платежа | Статус | Финальный | Значение | |---|---|---| | CREATED | нет | Создан, оплата не начата. Можно отменить по API. | | PENDING | нет | Покупатель начал оплату (счёт выставлен, 20 минут на оплату). | | SUCCESS | да | Оплачен, заполнен paidAt. | | FAILED | да | Отказ банка или отмена; деньги не списаны. | | EXPIRED | да | Время оплаты истекло. | | REFUNDED | да | Возвращён покупателю после SUCCESS (возвраты - через поддержку TabPay). | | CANCELED | да | Отменён мерчантом по API до начала оплаты; payUrl показывает «Счёт отменён». | Жизненный цикл: CREATED -> PENDING -> SUCCESS | FAILED | EXPIRED; из SUCCESS возможен переход в REFUNDED, из CREATED - в CANCELED (отмена по API). Переходов назад нет. Для повторной попытки оплаты создаётся НОВЫЙ платёж. Неизвестный коду статус в вебхуке подтверждать 200 и логировать - набор может расширяться. ## Вебхуки При каждом переходе платежа в финальный статус (SUCCESS, FAILED, EXPIRED, REFUNDED, CANCELED) TabPay шлёт POST на Webhook URL магазина (настраивается в кабинете, вкладка «Webhook»; там же лежит секрет подписи). Тело: ```json { "id": "6b9d2c88-4b1a-4f0e-9c37-1f2ab34cd561", "orderId": "order-1001", "status": "SUCCESS", "amountKopecks": 19900, "telegramId": "987654321", "metadata": { "productId": 42, "tariff": "month" }, "test": false } ``` Поле `test` есть в каждом вебхуке: `false` у боевых платежей, `true` у тестовых (кнопка «Отправить тестовый вебхук» или платёж магазина-песочницы). Проверять значение поля, а не его наличие. Требования к приёмнику: - публичный адрес (не приватная сеть), настоятельно рекомендуется HTTPS; - ответ 2xx в течение 5 секунд; редиректы (3xx) считаются неудачей; - сначала ответить 200, потом делать долгую работу (выдачу товара). Тестовый вебхук: кнопка «Отправить тестовый вебхук» на вкладке Webhook в кабинете шлёт уведомление, собранное как боевое (та же подпись X-Signature), но с полем "test": true. Обработчик должен ответить 200 и не выдавать товар. У боевых платежей "test": false. ## Тестовый режим (песочница) Для проверки интеграции без реальных денег создайте в кабинете (раздел «Магазины» -> «Добавить магазин» -> вкладка «Тестовый») магазин-песочницу: он активен сразу, без модерации, один на пользователя; ненужный удаляется в настройках магазина, после чего можно создать новый. Платежи создаются обычным API его ключом и помечены "isTest": true. На платёжной странице (payUrl) вместо СБП/карты - кнопки исхода: «Оплатить» (SUCCESS), «Отклонить» (FAILED), «Просрочить» (EXPIRED). Реальная оплата тестового платежа запрещена (409). Провайдер не участвует, деньги не двигаются, вебхук приходит с "test": true. Тестовые платежи не влияют на баланс и выплаты; в аналитике кабинета (GET /analytics/summary) они видны с пометкой «тестовые» (testPaidCount/testTurnoverKopecks) и в реальную выручку не входят. Полный рецепт: https://tabpay.org/docs/sandbox Рецепт для телеграм-ботов (aiogram/grammY) с готовым кодом: https://tabpay.org/docs/telegram-bot Повторы: если не было 2xx - повтор через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 24 ч (всего до 7 попыток). Тело между попытками не меняется. Обработка должна быть идемпотентной: пара (id, status) может прийти дважды - товар выдаётся один раз. ### Подпись вебхука (ОБЯЗАТЕЛЬНО проверять) В каждом вебхуке три заголовка подписи; ключ обеих схем - «Секрет подписи» магазина (вкладка «Webhook» в кабинете): - `X-Timestamp` - unix-время отправки в секундах; считается на КАЖДУЮ попытку доставки (тело при этом не меняется); - `X-Signature-V2` - HMAC-SHA256 от строки `{X-Timestamp}.{сырое тело}` (метка, точка, байты тела), hex строчными буквами. РЕКОМЕНДУЕМАЯ схема: проверка свежести метки (окно 5 минут в обе стороны) защищает от повторного проигрывания перехваченных вебхуков; - `X-Signature` - прежняя схема, HMAC-SHA256 только от сырого тела; оставлена для совместимости. КРИТИЧНО: проверять по сырым байтам тела ДО парсинга JSON - пересобранный JSON меняет порядок ключей и пробелы, подпись не сойдётся. Сравнивать функцией постоянного времени. Часы сервера - по NTP, иначе свежие вебхуки не пройдут окно допуска. Окно допуска не отменяет идемпотентность по (id, status): легитимные повторы приходят и через часы, со свежей меткой. Node.js (схема v2): ```js import { createHmac, timingSafeEqual } from 'node:crypto' function isValidWebhook(rawBody, headers, secret) { const ts = Number(headers['x-timestamp']) if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false const expected = createHmac('sha256', secret) .update(`${headers['x-timestamp']}.${rawBody}`) .digest('hex') const a = Buffer.from(expected) const b = Buffer.from(String(headers['x-signature-v2'] || '')) return a.length === b.length && timingSafeEqual(a, b) } // Express: app.post('/hook', express.raw({ type: 'application/json' }), handler) // В handler: isValidWebhook(req.body, req.headers, secret) ``` PHP (схема v2): ```php $rawBody = file_get_contents('php://input'); $ts = $_SERVER['HTTP_X_TIMESTAMP'] ?? ''; $ok = is_numeric($ts) && abs(time() - (int) $ts) <= 300 && hash_equals( hash_hmac('sha256', $ts . '.' . $rawBody, getenv('TABPAY_WEBHOOK_SECRET')), $_SERVER['HTTP_X_SIGNATURE_V2'] ?? '' ); ``` Python (схема v2): ```python import hashlib, hmac, time def is_valid_webhook(raw_body: bytes, timestamp: str, signature_v2: str, secret: str) -> bool: if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300: return False payload = timestamp.encode() + b"." + raw_body expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_v2 or "") # Flask: request.get_data() - сырые байты; заголовки X-Timestamp, X-Signature-V2 ``` ## Ошибки Единый формат: `{"statusCode": число, "message": строка или массив строк, "error": строка}`. При ошибках валидации message - массив со всеми проблемами. | Код | Когда | |---|---| | 400 | Тело не прошло валидацию (причины в message). | | 401 | Нет/неверный X-Api-Key или магазин не активен. | | 404 | Платёж не найден или чужой. | | 409 | Дубликат orderId, способ оплаты недоступен магазину либо отмена невозможна (оплата уже началась). | | 429 | Превышен лимит запросов. | | 5xx | Результат неизвестен: сначала GET по orderId, не повторять вслепую. | ## Лимиты 600 запросов в минуту на каждый метод API, считается по API-ключу (у GET /v1/balance свой лимит - 60 в минуту). Восстановление в течение минуты. Статусы узнавать вебхуками, не поллингом. ## Чек-лист правильной интеграции 1. Ключи в переменных окружения, не в коде. 2. Создание платежа: обработаны 409 (дубль orderId) и сетевые сбои (сверка по orderId перед повторным созданием). 3. Покупатель отправляется на payUrl. 4. Вебхук: проверка подписи v2 (X-Signature-V2 от «метка.тело» + свежесть X-Timestamp) по сырому телу, ответ 200 за 5 секунд, идемпотентная обработка (id, status). 5. Товар выдаётся только при status = SUCCESS с валидной подписью. 6. REFUNDED обрабатывается (товар/доступ отзывается или фиксируется возврат). 7. Заказ отменён покупателем до оплаты - платёж отменяется через POST /v1/payments/{id}/cancel; 409 на отмену означает «оплата ещё может пройти». 8. Сверка с учётной системой - списком GET /v1/payments за период, а не поштучным перебором. 9. Телеграм-бот: передавайте telegramId при создании платежа - в вебхуке он вернётся, и бот сразу знает, какому пользователю выдавать товар.