# QR-код адреса счёта Source: https://docs.oblodai.com/api-reference/платежи/qr-код-адреса-счёта /api-reference/openapi.json post /v1/payment/qr Возвращает QR адреса оплаты (по `uuid`/`order_id`) как PNG data:-URI — вставляется прямо в ``. # Доступные валюты и сети для приёма Source: https://docs.oblodai.com/api-reference/платежи/доступные-валюты-и-сети-для-приёма /api-reference/openapi.json post /v1/payment/services Список валют/сетей, которые можно принимать, с лимитами и комиссиями. Тело запроса — пустой `{}`. # История платежей Source: https://docs.oblodai.com/api-reference/платежи/история-платежей /api-reference/openapi.json post /v1/payment/history Список ваших платежей с пагинацией по курсору. Фильтры по датам `date_from`/`date_to`. # Отправить счёт на e-mail Source: https://docs.oblodai.com/api-reference/платежи/отправить-счёт-на-e-mail /api-reference/openapi.json post /v1/payment/send-email Шлёт покупателю письмо с кнопкой «Оплатить» для существующего платежа (по `uuid`/`order_id`). Адрес — поле `email` или `payer_email` платежа. Требует настроенный SMTP (иначе `email.disabled`). Повторная отправка тому же адресу по тому же платежу ограничена (не чаще раза в час; иначе `email.rate_limited`, 429). Чек об оплате отправляется автоматически на `payer_email`, когда платёж получен. # Создать платёж (счёт на оплату) Source: https://docs.oblodai.com/api-reference/платежи/создать-платёж-счёт-на-оплату /api-reference/openapi.json post /v1/payment Создаёт счёт и возвращает адрес + сумму к оплате и ссылку на страницу оплаты. **Как проще всего:** передайте `amount` (сумма), `currency` (валюта цены, напр. `USD`), `order_id` (ваш номер заказа). Если укажете `network` и `to_currency` — сразу зафиксируется конкретная монета/сеть. Если НЕ укажете — получится валюто-агностичная ссылка: клиент сам выберет валюту и сеть на странице оплаты. **Цена и расчёт — разные вещи.** `currency` говорит, сколько счёт СТОИТ: это может быть фиат (`USD`, `EUR`, `RUB`, `GBP`, `JPY` и ещё 18 валют) или любая монета. `to_currency` говорит, чем ПЛАТЯТ: **только крипта**. Фиата мы не храним, поэтому баланс, выплаты и возвраты всегда в монете — счёт на 5000 ₽ выставить можно, а получить за него можно USDT, TRX и т. д. Отсюда правило: если цена в фиате, то `to_currency` либо задаётся явно, либо не задаётся вовсе — вместе с `network` (тогда монету выберет покупатель). Цена в фиате + одна лишь `network`, без монеты, вернёт `payment.to_currency_required`: вывести монету из рублей неоткуда. У иены и воны (`JPY`, `KRW`) **нет копеек** — сумма пишется без дробной части (`"10000"`, не `"10000.00"`). Полный список валют цены — в `pricing_currencies` у `GET /v1/currencies`. **Идемпотентность:** повтор с тем же `order_id` вернёт тот же счёт (двойного счёта не будет). Необязательные удобства: `lifetime` (сколько секунд живёт счёт, 300–43200), `url_return`/`url_success` (куда вернуть клиента), `url_callback` (куда слать вебхук), `additional_data` (ваши приватные данные), `payer_email`, `accuracy_payment_percent` (допуск недо/переплаты 0–5%), `is_refresh` (оживить просроченный счёт по order_id). # Узнать статус платежа Source: https://docs.oblodai.com/api-reference/платежи/узнать-статус-платежа /api-reference/openapi.json post /v1/payment/info Передайте `uuid` (наш) ИЛИ `order_id` (ваш). Вернёт текущий статус и суммы. Если оба — приоритет у `order_id`. # Приём первого платежа Source: https://docs.oblodai.com/guides/accept-first-payment Разберём полный поток приёма платежа: от создания счёта до финального статуса, с объяснением, что происходит на каждом шаге и как на это реагировать. Если нужен просто «завести за 5 минут» — см. [Быстрый старт](/quickstart). Здесь — подробнее. *** ## Как это работает ``` 1. Вы создаёте счёт (POST /v1/payment) ↓ получаете uuid, адрес, ожидаемую сумму 2. Покупатель отправляет средства на адрес ↓ 3. Шлюз видит транзакцию и набирает подтверждения сети ↓ (число зависит от суммы и сети) 4. Порог достигнут → счёт становится оплаченным ↓ вам приходит вебхук invoice.paid 5. Недоплата/переплата обрабатываются по вашим настройкам accuracy и autorefund ``` *** ```python Python theme={null} payment = call("/v1/payment", { "amount": "10", "currency": "USD", # валюта ЦЕНЫ: USD, EUR, RUB… или монета "order_id": "order-1", # ваш бизнес-ключ — задавайте всегда, он дедуплицирует "to_currency": "USDT", "network": "tron", "lifetime": 3600, "payer_email": "buyer@example.com", # необязательно: включает АВТОЧЕК после оплаты }, idempotency_key="pay-order-1")["result"] # Idempotency-Key стабилен при ретраях — не генерируйте новый на каждую попытку ``` ```js Node.js theme={null} const payment = (await call("/v1/payment", { amount: "10", currency: "USD", // валюта ЦЕНЫ: USD, EUR, RUB… или монета order_id: "order-1", // ваш бизнес-ключ — задавайте всегда, он дедуплицирует to_currency: "USDT", network: "tron", lifetime: 3600, payer_email: "buyer@example.com", // необязательно: включает АВТОЧЕК после оплаты // Idempotency-Key стабилен при ретраях — не генерируйте новый на каждую попытку; передаём третьим аргументом }, "pay-order-1")).result; ``` Про два механизма защиты от дублей (`Idempotency-Key` + `order_id`) — [Устойчивый клиент](/guides/resilient-client#главный-принцип). Про авточек на `payer_email` — [Счёт и чек на почту](/guides/email-invoices). Из ответа вам нужны: * `address` — адрес, на который платит покупатель; * `payer_amount` + `payer_currency` — сколько и в какой крипте нужно отправить; * `url` — ссылка на hosted‑страницу оплаты (можно просто отправить покупателя туда); * `uuid` — сохраните его в связке с вашим заказом; * `payer_address` — **появляется после оплаты**: адрес, с которого реально пришли деньги. Это дефолтный адрес возврата, поэтому [возврат](/guides/refunds) можно делать, не спрашивая адрес у покупателя. Полное описание полей — [Объект платежа](/reference/payment-object). Два варианта: 1. **Отправить на hosted‑страницу** по `url` — там уже есть адрес, QR, таймер и статус. Ничего рисовать не нужно. 2. **Показать у себя** — возьмите `address` и `address_qr_code` (готовый `data:`‑URI QR) из ответа. Страница может отслеживать статус без секрета мерчанта — через публичный [`GET /v1/pay/{id}`](/reference/pay-get). Счёт проходит по статусам ([`payment_status`](/reference/payment-object#статусы-payment_status)): | Статус | Что значит | Ваша реакция | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `select` | Валюто‑агностичный счёт (`is_multi: true`): покупатель **ещё не выбрал монету и сеть**. Адреса и `payer_currency` пока нет — это нормально. | Ничего, ждём выбора на странице оплаты. → [Режим 3](/guides/payment-modes#режим-3-валюто‑агностичный-deferred-счёт) | | `check` | Счёт создан, ждём оплату. | Ничего, ждём. | | `confirm_check` | Транзакцию видим, ждём подтверждений. | Можно показать «платёж получен, подтверждается». | | `wrong_amount_waiting` | Пришла недоплата, срок ещё не вышел. | Ждём доплату (если разрешена). | | `paid` | Оплачено в пределах допуска. | **Выдать товар/услугу.** | | `paid_over` | Переплата сверх допуска. | Выдать; излишек вернётся, если включён [автовозврат](/reference/payment-autorefund). | | `wrong_amount` | Недоплата, срок вышел. | Не выдавать; средства вернутся при автовозврате. | | `cancel` | Счёт истёк или отменён. | Закрыть заказ. | Терминальные статусы (`is_final: true`): `paid`, `paid_over`, `wrong_amount`, истёкшие/отменённые. Правильный способ узнать об оплате — **вебхук** `invoice.paid`, а не постоянный опрос `/info`. Настройте приёмник по инструкции [Настройка вебхуков](/guides/webhooks-setup). Опрос [`POST /v1/payment/info`](/reference/payment-info) держите как резервный механизм. Ключевое правило обработки: **дедуплицируйте по `uuid` + `status`** и **выдавайте товар только один раз** — вебхук может прийти повторно. *** ## Важные нюансы * **Курс фиксируется при создании** и действует до `rate_expires_at`. Для долгоживущих ссылок курс лениво перезапрашивается при открытии страницы, пока счёт не оплачен и не истёк. После начала оплаты адрес и сумма не меняются. * **Минимум сети.** На дорогих сетях (Ethereum, Bitcoin) есть минимальная сумма; платёж ниже вернёт `payment.below_minimum` уже при создании. * **Подтверждения зависят от суммы.** Крупный платёж требует больше подтверждений (reorg‑безопасность), мелкий — меньше. * **Идемпотентность — два механизма.** HTTP‑заголовок **`Idempotency-Key`** (одинаковый во всех попытках одного действия; повтор вернёт тот же ответ и `Idempotent-Replayed: true`) и **`order_id`** (ваш бизнес‑ключ: повтор создания с тем же `order_id` вернёт существующий живой счёт, а не создаст второй). См. [Идемпотентность](/reference/basics-idempotency). *** ## Связанные страницы а что, если не фиксировать валюту заранее? как настроить допуски. постоянный адрес вместо разовых счетов. принять платёж вообще без бэкенда. автоматический чек плательщику. создать до 5000 счетов одним запросом. # Интеграция с AI Source: https://docs.oblodai.com/guides/ai-integration Подключите Oblodai к своему приложению за минуты с помощью Claude, ChatGPT, Cursor или Copilot: LLM-версии документации, MCP-сервер и готовые промпты. Документация Oblodai сделана дружественной к LLM. Всю справку API можно передать любому современному AI-ассистенту и получить рабочую интеграцию на любом языке — PHP, Node.js, Python, Go, Rust — за минуты. Справка API при этом **генерируется из кода шлюза**, так что ассистент всегда видит актуальные схемы, а не устаревший пересказ. ## Машиночитаемая документация Мы публикуем стандартные endpoint'ы [llmstxt.org](https://llmstxt.org): | Endpoint | Назначение | | ---------------------------------------------------------- | ------------------------------------------------------ | | [`/llms.txt`](https://docs.oblodai.com/llms.txt) | Краткий индекс всей документации со ссылками | | [`/llms-full.txt`](https://docs.oblodai.com/llms-full.txt) | Вся документация одним файлом — для вставки в чат с AI | | `/{страница}.md` | Любая страница в сыром Markdown: добавьте `.md` к URL | Каждая HTML-страница содержит ``, поэтому AI-краулеры находят Markdown-версию сами. В меню каждой страницы есть «Copy page» и открытие прямо в ChatGPT/Claude. `llms-full.txt` — большой файл (около мегабайта). Для моделей с небольшим контекстом удобнее MCP-сервер ниже: ассистент подтянет только нужные страницы. ## MCP-сервер документации Агенты с поддержкой Model Context Protocol подключаются напрямую: ```text theme={null} https://docs.oblodai.com/mcp ``` Endpoint публичный, **только на чтение** и не требует ключа мерчанта. Он отдаёт исключительно содержимое документации: через него **нельзя** проверить баланс, создать платёж или перевести средства. Конфигурация для MCP-клиента (транспорт — Streamable HTTP): ```json theme={null} { "mcpServers": { "oblodai-docs": { "url": "https://docs.oblodai.com/mcp" } } } ``` Инструменты сервера: | Инструмент | Когда агенту его использовать | | ------------------------------- | -------------------------------------------------------------------- | | `search_oblodai` | Поиск по всей документации: фрагменты с прямыми ссылками на страницы | | `query_docs_filesystem_oblodai` | Прочитать полную Markdown-версию конкретной страницы | | `submit_feedback` | Оставить отзыв о документации | Документационный MCP — источник знаний, а не пульт управления кошельком. Не передавайте в аргументах инструментов API-ключи, секреты вебхуков и данные клиентов. ## Быстрый старт с Claude или ChatGPT **Шаг 1 — передайте документацию.** Вставьте содержимое [`llms-full.txt`](https://docs.oblodai.com/llms-full.txt) первым сообщением, или просто дайте ссылку, если модель умеет ходить по URL, или подключите MCP-сервер. **Шаг 2 — опишите свой стек:** ```text theme={null} Я делаю приложение на Laravel 11. Мне нужно: 1. Создать платёж под заказ (сумма в USD, пользователь платит USDT TRC-20) 2. Обработать вебхук и зачислить баланс пользователю 3. Сохранять платежи в таблице payments Используй API Oblodai из документации выше. Включи HMAC-подпись запросов, проверку подписи вебхука и идемпотентную обработку. ``` **Шаг 3 — проверьте и протестируйте.** Перед запуском сгенерированного кода: * подпись запроса — `hex(HMAC-SHA256(секрет, "\n<МЕТОД>\n<путь>\n<тело>"))`, склейка ровно переводами строк ([как подписать запрос](/guides/signing-requests)); * подпись вебхука сверяется **константным по времени** сравнением (`hash_equals`, `hmac.compare_digest`, `crypto.timingSafeEqual`) — никогда `==` ([безопасность вебхуков](/guides/webhooks-security)); * обработчик вебхука идемпотентен: дедуп по `uuid`+`status` до зачисления; * прогоните всё в [песочнице](/guides/testing): dev-store с тестовыми деньгами, симуляцией депозитов (`POST /v1/sandbox/deposit`) и повтором вебхуков — реальные средства для проверки не нужны. Никогда не выпускайте сгенерированный AI код для платежей без ручного ревью логики подписи и проверки вебхуков. Это критические границы безопасности. ## Интеграции с IDE ### Cursor Добавьте документацию как источник docs: `Settings → Features → Docs → Add new doc` → `https://docs.oblodai.com`. Затем в чате: ```text theme={null} @Oblodai сгенерируй обработчик вебхука в Next.js App Router с проверкой подписи и идемпотентным зачислением ``` ### GitHub Copilot Copilot Chat умеет читать `llms-full.txt` напрямую: ```text theme={null} #fetch https://docs.oblodai.com/llms-full.txt Используя документацию Oblodai выше, реализуй endpoint выплат в Express, который выводит USDT TRC-20 на адрес пользователя. ``` ### Windsurf / Continue / другие Любой ассистент с контекстом по URL или вложением файла работает так же: приложите `llms-full.txt` (или подключите MCP) и опишите задачу. ## Claude API (свой агент) Если вы строите собственного агента, который должен помогать с интеграцией Oblodai — подключите MCP-сервер документации или вставьте `llms-full.txt` в системный промпт: ```python theme={null} from anthropic import Anthropic import urllib.request docs = urllib.request.urlopen( "https://docs.oblodai.com/llms-full.txt" ).read().decode() client = Anthropic() response = client.messages.create( model="claude-sonnet-5", max_tokens=4096, system=f"Ты — ассистент интеграции Oblodai. Отвечай по справке ниже.\n\n\n{docs}\n", messages=[{"role": "user", "content": "Напиши функцию на Python, создающую счёт на 10 USDT"}], ) print(response.content[0].text) ``` ## Промпты, которые работают хорошо **Полная бэкенд-интеграция:** ```text theme={null} Собери сервис на Node.js + Express с двумя роутами: - POST /checkout → создаёт платёж Oblodai и возвращает url страницы оплаты - POST /webhook/oblodai → проверяет подпись и помечает заказ оплаченным TypeScript, Zod для валидации. Подпись запросов и вебхуков — по документации. ``` **Постоянные адреса пополнения (статические кошельки):** ```text theme={null} У меня Django-приложение, пользователи пополняют баланс в USDT TRC-20. У каждого пользователя должен быть постоянный адрес пополнения. Реализуй это на статических кошельках Oblodai (POST /v1/wallet, order_id = id пользователя) и напиши обработчик вебхука, зачисляющий баланс при депозите. ``` **Утилита выплат:** ```text theme={null} Напиши CLI на Go: принимает валюту, сеть, сумму и адрес, создаёт выплату через POST /v1/payout (ключ — из переменной окружения), затем опрашивает POST /v1/payout/info до финального статуса. ``` ## Лучшие практики * **Начинайте с `llms-full.txt` или MCP** — там нет HTML-шума, только содержание. * **Чётко описывайте стек** — фреймворк, версия языка, ORM. * **Просите тесты** — AI хорошо генерирует unit-тесты для логики подписи. * **Перепроверяйте ветки ошибок** — AI их иногда пропускает; сверьтесь с [каталогом ошибок](/reference/errors-catalog). * **Код подписи ревьюйте руками** — это единственная часть, которую обязательно сделать точно. * **Гоняйте в песочнице до прода** — dev-store покрывает весь цикл: платежи, недоплаты, выплаты, вебхуки ([тестирование](/guides/testing)). * **API меняется — документация обновляется сама** (спека генерируется из кода), поэтому при повторном заходе просто перечитайте `llms-full.txt` заново. # Модель баланса и движение средств Source: https://docs.oblodai.com/guides/balance-and-funds Отдельные методы (приём, выплаты, автовывод, переводы) станут понятнее, если увидеть общую картину: откуда деньги приходят на баланс, в каких они состояниях и какими путями уходят. Эта страница — концептуальная карта. За деталями каждого метода — ссылки в [Справочник](/reference/overview). *** ## Общая схема ```mermaid theme={null} flowchart LR IN1["Инвойс /v1/payment
(минус комиссия платформы)"] --> HELD IN2["Статический кошелёк /v1/wallet
(минус комиссия платформы)"] --> HELD HELD["maturing
(зачислено, ещё не выводится)"] -->|"дозревание"| AV["available
(расходуемый баланс)"] AV --> P["Выплата
/v1/payout"] AV --> AW["Автовывод
/v1/auto-withdraw"] AV --> R["Возврат
/v1/payment/refund"] AV --> T["Перевод на личный кошелёк
/v1/transfer/to-personal"] AV --> SP["Сплит — доля партнёрам
(авто, после окна удержания)"] ``` Ключевая идея: **средства попадают на баланс сразу, но не всегда сразу выводимы**. Между «зачислено» и «можно вывести» стоит дозревание (maturity). *** ## Приход: два канала Деньги на баланс мерчанта приходят двумя разными путями — и ведут они себя по-разному. | Канал | Что это | Комиссия платформы | Вебхук | | ----------------------------------------------- | --------------------------- | ----------------------------------------------------- | ------------- | | [Инвойс](/reference/payment-create) | Разовый счёт на сумму | **Удерживается** (по умолч. 1.5 % + \$0.30/платёж) | `invoice.*` | | [Статический кошелёк](/reference/wallet-create) | Постоянный адрес пополнения | **Удерживается** (по вашей ставке, по умолч. \~1.5 %) | `wallet.paid` | Оба канала зачисляют **нетто** — за вычетом комиссии платформы. В вебхуке `wallet.paid` поле `payment_amount` — это брутто (что пришло на адрес); на баланс попадёт меньше на размер комиссии. Сверяйтесь по [`/v1/balance`](/reference/balance). Подробнее про разницу — [Объект кошелька](/reference/wallet-object) и [Статические кошельки](/guides/static-wallets). *** ## Состояния средств на балансе Средства на балансе бывают в трёх состояниях. Метод [`/v1/balance`](/reference/balance) показывает **одно число** (available + maturing вместе; held не входит), и не всё, что показано, выводимо прямо сейчас. | Состояние | Что значит | Видно в `/v1/balance`? | Выводится? | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------- | | **available (зрелое)** | Расходуемый остаток, набравший подтверждения. | Да | Да | | **maturing (незрелое)** | Депозит зачислен, но ещё не набрал подтверждений (reorg-безопасность). | Да (входит в `balance`) | **Нет** | | **held (замороженное)** | Зарезервировано под операцию: `payout_held` под выплату в процессе или под невостребованный [крипто-чек](/guides/payout-links). | Нет | Нет | ### Почему «баланс есть, а вывести нельзя» Самый частый источник путаницы. Свежий депозит **сразу** отражается в `balance`, но пока он под maturity-холдом, выплата на сумму, включающую незрелые средства, вернёт **`409 payout.funds_maturing`**. То есть выводимый остаток может быть временно меньше показанного. **Что делать.** Важная честная оговорка: API **не отдаёт разбивку** available/maturing — [`/v1/balance`](/reference/balance) показывает одно число, в которое незрелые средства уже входят, поэтому «вычислить зрелую часть» одним вызовом нельзя. Рабочих стратегий две: * **Повторять выплату с backoff по `409 payout.funds_maturing`** — незрелое станет зрелым автоматически, и повтор пройдёт. Готовый код ретрая уже есть в [рецепте устойчивого клиента](/guides/resilient-client#не-все-409-финальны). * **Выводить консервативно** — сумму заведомо меньше остатка без учёта свежих депозитов. Оценить, сколько ждать, можно так: reorg‑безопасный порог подтверждений сети — поле `min_confirmations` в [`GET /v1/currencies`](/reference/currencies); прогресс конкретного платежа — `confirmations` / `required_confirmations` в [`/v1/payment/info`](/reference/payment-object). Учтите, что фактический порог зависит и от суммы: крупный платёж зреет дольше. *** ## Расход: пять путей Списать средства с баланса можно пятью способами. Три из них (выплата, возврат, перевод) инициируете вы вызовом API; два (автовывод и сплит) срабатывают **сами**, по заранее настроенному правилу. | Путь | Кто инициирует | Когда используется | | ---------------------------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------ | | [**Выплата**](/reference/payout-create) | Вы | Вывод на внешний адрес. По API-ключу авто-одобряется, уходит сразу. | | [**Автовывод**](/reference/auto-withdraw) | **Шлюз, авто** | Автоматически выводит чистую сумму депозита на заданный адрес. | | [**Возврат**](/reference/payment-refund) | Вы | Вернуть средства оплаченного платежа. Списывает **всю** сумму, которую заплатил покупатель. | | [**Перевод на личный кошелёк**](/reference/transfer-to-personal) | Вы | Перевод владельцу аккаунта (общий для его магазинов). | | [**Сплит**](/reference/split-rule) | **Шлюз, авто** | Доля каждого входящего платежа уходит партнёрам по вашему правилу. → [Сплит‑платежи](/guides/split-payments) | **Сплит легко упустить из виду при сверке баланса.** В отличие от выплаты, у него нет вашего вызова API: вы один раз настроили правило «20 % партнёру», и дальше деньги уходят сами — но **не сразу**, а после окна удержания `refund_hold_hours`. Из-за этой задержки баланс может «худеть» на суммы, которых вы не ждёте сегодня: это рассчитались платежи, пришедшие несколько дней назад. Окно нужно потому, что возврат списывает с вас **всю** сумму покупателя — если бы доли ушли партнёрам сразу, на возврат бы не осталось. → [Сплит‑платежи](/guides/split-payments) **Односторонние двери.** Перевод на личный кошелёк **вводит** средства В личный кошелёк, но вывод оттуда через API закрыт — только в кабинете под 2FA. Аналогично ротация ключа — только в кабинете. Это осознанные ограничения безопасности, см. [Безопасность в проде](/guides/production-security). *** ## Комиссии: кто и за что платит В движении средств участвуют две разные комиссии — не путайте их. | Комиссия | Что это | Где настраивается | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | | **Наша (комиссия платформы)** | Доля Oblodai с приёма (у реферала — 1.4 %). Инвойс: по умолчанию **1.5 % + \$0.30/платёж**. Статический кошелёк: те же **1.5 %**, но **без** фиксированных \$0.30. | Ставка мерчанта (override или глобальная); кто несёт её при возврате — [`refund-fee-config`](/reference/payout-refund-fee-config). | | **Сетевая (газ)** | Плата блокчейн-сети за транзакцию выплаты/возврата. | [`fee-config`](/reference/payout-fee-config) (кто несёт при выплате). | Предрасчёт по конкретной выплате (сколько уйдёт с баланса, сколько получит адрес) — без создания: [`/v1/payout/calculate`](/reference/payout-calculate). *** ## Защита от волатильности (VRCS) Если вы принимаете волатильные активы, но хотите держать баланс в стабильной монете, включите [VRCS](/reference/vrcs) — он авто-конвертирует волатильные депозиты в USDT при зачислении. Это влияет на то, в какой валюте окажется ваш `available`. *** ## Реферальные начисления Отдельный «карман» — реферальные доходы. Они начисляются как доля **нашей** комиссии с приведённых мерчантов и показываются в [`/v1/referral/info`](/reference/referral-info) в **minor-единицах**. Это не тот же поток, что торговый баланс. → [Форматы сумм](/reference/basics-money) *** ## Как это выглядит в цифрах (пример) 1. Покупатель платит по инвойсу 100 USDT. Наша комиссия 1.5 % + \$0.30 → удержится ≈ 1.80 → на баланс зачислится **≈ 98.2 USDT** (сразу как maturing). 2. Через нужное число подтверждений 98.2 USDT становятся **available**. 3. Вы выводите 50 USDT (`/v1/payout`). Если сетевая комиссия на получателе — адрес получит 50 минус газ; с баланса спишется 50. Остаток available — ≈ 48.2 USDT. 4. Если бы те же 100 USDT пришли на **статический кошелёк**, комиссия удержалась бы **тоже** (по вашей ставке, по умолчанию \~1.5 %) — на баланс легло бы ≈ 98.5 USDT (фиксированные \$0.30 берутся с инвойса, не с пополнения кошелька). Точные суммы и знаки зависят от валюты/сети — всегда сверяйтесь с ответом [`/v1/payout/calculate`](/reference/payout-calculate) и [`/v1/balance`](/reference/balance). *** ## Связанные страницы как обрабатывать maturing и повторы. available, maturing, held, газ, VRCS. # Массовые операции (батчи) Source: https://docs.oblodai.com/guides/batch-operations Батч — это способ отправить **до 5000 операций одним подписанным запросом**. Работает для трёх видов операций: создание платежей, возвраты и выплаты. Главное, что нужно понять сразу: **батч — это штатный способ не упираться в rate limit.** Лимит частоты считается по HTTP‑запросам (по умолчанию 120 запросов/мин на IP). Если вы отправляете 5000 выплат циклом — это 5000 запросов, и вы гарантированно упрётесь в `429` и растянете работу на часы. Тот же список, отправленный батчем, — это **один** запрос и одна подпись. | Как отправить 5000 выплат | Запросов к API | Что будет | | -------------------------------------- | -------------- | ------------------------------------------------- | | Циклом по одной | 5000 | `429 rate limit`, ретраи, часы ожидания | | Пачками по 100 через `/v1/payout/mass` | 50 | Лучше, но всё ещё много запросов и ручная нарезка | | **Батчем `/v1/payout/batch`** | **1** | Приняли за секунду, обрабатывается асинхронно | *** ## Как это устроено: асинхронность Батч **не** обрабатывается синхронно. Шлюз не держит ваше соединение, пока проводит 5000 выплат — это заняло бы минуты и любой таймаут убил бы запрос. Вместо этого: ``` 1. Вы отправляете батч (POST /v1/payout/batch) ↓ сразу же, за миллисекунды 2. Шлюз отвечает: batch_id + status: "pending" ↓ дальше шлюз обрабатывает элементы у себя, в фоне 3. Вы опрашиваете POST /v1/batch/info по batch_id ↓ pending → processing → completed 4. Статус completed → разбираете результаты поэлементно ``` То есть ответ на отправку **не содержит результатов** — только квитанцию с `batch_id`. Результаты появляются позже, в `/v1/batch/info`. *** ## Шаг 1. Отправить батч Три эндпоинта, по одному на вид операции. Внутри массива лежат **ровно те же объекты**, что вы отправили бы поштучно: | Эндпоинт | Ключ массива | Элемент = тело метода | | ------------------------ | ------------ | ------------------------------------------------------ | | `POST /v1/payment/batch` | `payments` | [`POST /v1/payment`](/reference/payment-create) | | `POST /v1/refund/batch` | `refunds` | [`POST /v1/payment/refund`](/reference/payment-refund) | | `POST /v1/payout/batch` | `payouts` | [`POST /v1/payout`](/reference/payout-create) | ```python Python theme={null} resp = call("/v1/payout/batch", { "payouts": [ {"amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "p-1"}, {"amount": "10", "currency": "USDT", "network": "tron", "address": "TZZ...", "order_id": "p-2"}, # … до 5000 элементов ], "on_error": "continue", })["result"] batch_id = resp["batch_id"] print(resp) # {"batch_id": "b3f1…", "kind": "payout", "count": 2, "status": "pending"} ``` ```js Node.js theme={null} const resp = (await call("/v1/payout/batch", { payouts: [ { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "p-1" }, { amount: "10", currency: "USDT", network: "tron", address: "TZZ...", order_id: "p-2" }, // … до 5000 элементов ], on_error: "continue", })).result; const batchId = resp.batch_id; console.log(resp); // {"batch_id": "b3f1…", "kind": "payout", "count": 2, "status": "pending"} ``` Сохраните `batch_id` — без него результаты не забрать. Элементы батча платежей поддерживают **всё то же**, что и одиночный `POST /v1/payment`, — включая цену в фиате, причём в разных валютах внутри одного батча: ```python Python theme={null} call("/v1/payment/batch", {"payments": [ {"amount": "5000", "currency": "RUB", "order_id": "o-1", "to_currency": "USDT", "network": "tron"}, {"amount": "100", "currency": "EUR", "order_id": "o-2", "to_currency": "USDT", "network": "tron"}, ]}) ``` ```js Node.js theme={null} await call("/v1/payment/batch", { payments: [ { amount: "5000", currency: "RUB", order_id: "o-1", to_currency: "USDT", network: "tron" }, { amount: "100", currency: "EUR", order_id: "o-2", to_currency: "USDT", network: "tron" }, ]}); ``` → [Три режима создания счёта](/guides/payment-modes#цену-можно-назначать-не-только-в-долларах) ### `on_error`: что делать при ошибке в элементе | Значение | Поведение | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `"continue"` (**по умолчанию**) | Ошибочный элемент помечается `error`, остальные обрабатываются. Частичный успех — норма. | | `"stop"` | После первой ошибки обработка прекращается. Оставшиеся элементы **тоже получают статус `error`** — с пояснением `"skipped: batch stopped after an earlier failure"` — и попадают в счётчик `failed`. | **Отдельного статуса `skipped` нет.** Непройденный элемент отличается от настоящей ошибки только текстом в поле `error`. Поэтому при `on_error: "stop"` число `failed` включает и те элементы, до которых обработка просто не дошла, — не считайте их все отказами. Берите `continue`, если элементы независимы (типичный случай: выплаты разным людям). Берите `stop`, если пачка осмысленна только целиком и вы хотите разобраться до того, как уйдут остальные деньги. Как `continue` выглядит на практике: батч из 3 платежей, один элемент с несуществующей парой валюта/сеть → ```json theme={null} {"status": "completed", "total": 3, "succeeded": 2, "failed": 1, "items": [ {"idx": 0, "status": "done"}, {"idx": 1, "status": "error", "error": "unsupported network for this currency"}, {"idx": 2, "status": "done"} ]} ``` Ошибка в середине **не остановила** остальные — элементы 0 и 2 создались. **`stop` — это не откат.** Элементы, которые успели пройти **до** ошибки, уже выполнены и назад не откатываются: деньги ушли. `stop` лишь не даёт выполнить оставшиеся. Если вам нужна семантика «всё или ничего» — её нет: проверяйте данные до отправки. Как выглядит `stop` на практике — тот же батч из 3 платежей, ошибка во втором: ```json theme={null} {"status": "completed", "total": 3, "succeeded": 1, "failed": 2, "items": [ {"idx": 0, "status": "done"}, {"idx": 1, "status": "error", "error": "unsupported network for this currency"}, {"idx": 2, "status": "error", "error": "skipped: batch stopped after an earlier failure"} ]} ``` Элемент 0 создался и остался. Элемент 2 не выполнялся — но формально он тоже `error`. *** ## Шаг 2. Опрашивать статус ```python Python theme={null} import time while True: info = call("/v1/batch/info", {"batch_id": batch_id, "limit": 100, "offset": 0})["result"] if info["status"] == "completed": break time.sleep(5) # не долбите чаще, чем нужно, — это тоже запросы ``` ```js Node.js theme={null} let info; while (true) { info = (await call("/v1/batch/info", { batch_id: batchId, limit: 100, offset: 0 })).result; if (info.status === "completed") break; await new Promise((r) => setTimeout(r, 5000)); // не долбите чаще, чем нужно, — это тоже запросы } ``` Статусы батча: `pending` (принят, ещё не начали) → `processing` (идёт обработка) → `completed` (обработаны все элементы, которые должны были обработаться). `completed` **не** значит «всё удалось». Оно значит «шлюз закончил». Сколько элементов прошло, а сколько нет — смотрите в `succeeded` / `failed`. *** ## Шаг 3. Разобрать результаты поэлементно Ответ `/v1/batch/info`: ```json theme={null} { "batch_id": "b3f1…", "kind": "payout", "status": "completed", "on_error": "continue", "total": 1000, "succeeded": 998, "failed": 2, "items": [ {"idx": 0, "status": "done", "order_id": "p-1", "result": { "uuid": "…", "status": "process" }}, {"idx": 1, "status": "error", "order_id": "p-2", "error": "available balance is less than the requested amount"} ] } ``` * `idx` — позиция элемента **в исходном массиве**, который вы отправили. По нему вы сопоставляете результат со своей строкой в БД. * `result` — то, что вернул бы одиночный вызов (полный объект платежа/выплаты/возврата, с `uuid`). * `error` — строка с причиной, если элемент не прошёл. Статусы **элемента** (`items[].status`) — не путайте их со статусом батча: | `items[].status` | Значение | | ---------------- | ------------------------------------------------ | | `done` | Элемент выполнен. В `result` — созданный объект. | | `error` | Элемент не прошёл. Причина — в `error`. | Статуса `skipped` **нет**. Элемент, до которого обработка не дошла (при `on_error: "stop"`), тоже получает `error` — с текстом `"skipped: batch stopped after an earlier failure"`. Отличить его от настоящего отказа можно только по этому тексту: ```python Python theme={null} SKIPPED = "skipped: batch stopped after an earlier failure" ``` ```js Node.js theme={null} const SKIPPED = "skipped: batch stopped after an earlier failure"; ``` **Ветвитесь по `status`, а не по тексту `error`.** Текст в `error` — человекочитаемое сообщение, а не код ошибки: он может измениться. Единственное исключение — проверка на строку выше, если вам важно отличить «не дошли» от «не прошло». `items` **постраничные** — их может быть 5000. Листайте через `limit`/`offset`: ```python Python theme={null} def batch_items(batch_id, page=100): offset, out = 0, [] while True: info = call("/v1/batch/info", {"batch_id": batch_id, "limit": page, "offset": offset})["result"] out += info["items"] offset += page if offset >= info["total"]: return info, out info, items = batch_items(batch_id) print(f"{info['succeeded']} ок, {info['failed']} с ошибкой") SKIPPED = "skipped: batch stopped after an earlier failure" for item in items: if item["status"] == "done": save_payout_uuid(item["order_id"], item["result"]["uuid"]) # ваш код elif item["error"] == SKIPPED: # до элемента не дошли (on_error: "stop") requeue(item["order_id"]) # ваш код: отправить в следующем батче else: mark_failed(item["order_id"], item["error"]) # ваш код ``` ```js Node.js theme={null} async function batchItems(batchId, page = 100) { let offset = 0; const out = []; while (true) { const info = (await call("/v1/batch/info", { batch_id: batchId, limit: page, offset })).result; out.push(...info.items); offset += page; if (offset >= info.total) return [info, out]; } } const [info, items] = await batchItems(batchId); console.log(`${info.succeeded} ок, ${info.failed} с ошибкой`); const SKIPPED = "skipped: batch stopped after an earlier failure"; for (const item of items) { if (item.status === "done") { await savePayoutUuid(item.order_id, item.result.uuid); // ваш код } else if (item.error === SKIPPED) { // до элемента не дошли (on_error: "stop") await requeue(item.order_id); // ваш код: отправить в следующем батче } else { await markFailed(item.order_id, item.error); // ваш код } } ``` ### Node.js: полный цикл ```js theme={null} const submitted = await call("/v1/payout/batch", { payouts: rows.map((r) => ({ amount: r.amount, currency: "USDT", network: "tron", address: r.address, order_id: r.orderId, })), on_error: "continue", }); const batchId = submitted.result.batch_id; // поллинг до completed let info; do { await new Promise((r) => setTimeout(r, 5000)); info = (await call("/v1/batch/info", { batch_id: batchId, limit: 100, offset: 0 })).result; } while (info.status !== "completed"); // постранично собрать элементы const items = []; for (let offset = 0; offset < info.total; offset += 100) { const page = (await call("/v1/batch/info", { batch_id: batchId, limit: 100, offset })).result; items.push(...page.items); } const SKIPPED = "skipped: batch stopped after an earlier failure"; for (const item of items) { // done = выплата создана (status: "process"); финал придёт вебхуком payout.confirmed/payout.failed if (item.status === "done") await savePayoutUuid(item.order_id, item.result.uuid); else if (item.error === SKIPPED) await requeue(item.order_id); // не дошли (on_error: "stop") else await markFailed(item.order_id, item.error); } ``` *** ## Шаг 4 (для выплат). `done` ≠ выплачено Для батча выплат `items[].status: "done"` означает лишь, что выплата **создана**: в `result` она приходит со `status: "process"` (это видно в примере ответа выше). Финальные статусы — `paid`, `fail` или `cancel` — наступают позже, когда шлюз отправляет транзакцию в сеть, и выплата на этом этапе всё ещё может упасть. Батч результат **не переоценивает**: выплата, упавшая после создания, в `/v1/batch/info` навсегда останется `done`. Смотреть на батч как на источник финального статуса нельзя. Дождаться финала можно двумя способами: * **Вебхуки** `payout.confirmed` (успех) / `payout.failed` — см. [Настройка вебхуков](/guides/webhooks-setup). * **Поллинг** [`POST /v1/payout/info`](/reference/payout-info) по сохранённому `result.uuid` — до `is_final: true` (статусы и признак финальности — в [объекте выплаты](/reference/payout-object)). Именно поэтому в примерах выше мы сохраняем `uuid` каждого `done`-элемента: это ваш ключ для сверки финального статуса. Для батча **платежей** аналогично: `done` = счёт создан, об оплате сообщат `invoice.*`‑вебхуки. *** ## Идемпотентность батча Работает на **двух** уровнях, и оба стоит использовать: 1. **Заголовок `Idempotency-Key` на самом запросе батча.** Защищает от «отправил батч, ответ потерялся, отправил ещё раз» — повтор с тем же ключом вернёт **тот же `batch_id`**, а не создаст второй батч из тех же 5000 выплат. Это ровно тот сценарий, где ошибка стоит очень дорого. 2. **`order_id` внутри каждого элемента.** Дедуплицирует конкретную операцию (для выплат `order_id` обязателен). Даже если батч каким‑то образом продублируется, элементы с теми же `order_id` не уйдут дважды. → [Идемпотентность](/reference/basics-idempotency) *** ## Ошибки уровня батча Отклоняется **весь** запрос (элементы даже не начинают обрабатываться): | Код | Значение | Что делать | | --------------------- | ---------------------------------- | ---------------------------------------------------------- | | `400 batch.empty` | Массив пуст. | Не отправляйте пустой батч. | | `400 batch.too_large` | Больше **5000** элементов. | Нарежьте на батчи по 5000. | | `400 batch.bad_id` | `batch_id` некорректного формата. | Проверьте, что передаёте `batch_id` из ответа на отправку. | | `404 batch.not_found` | Батч с таким `batch_id` не найден. | Проверьте `batch_id`; он принадлежит вашему мерчанту. | | `503 batch.disabled` | Батчи временно выключены. | Временная ошибка — повторите с backoff. | Ошибки **отдельных элементов** сюда не попадают: они лежат в `items[].error` и не роняют весь запрос (при `on_error: "continue"`). *** ## Что осталось от `/v1/payout/mass` [`POST /v1/payout/mass`](/reference/payout-mass) (до 100 выплат, **синхронно**) продолжает работать, но это **легаси‑путь**. Он оправдан ровно в одном случае: пачка маленькая и вам нужен результат **прямо в ответе**, без поллинга. Для всего остального — батч. | | `/v1/payout/mass` | `/v1/payout/batch` | | ------------------- | ------------------------------ | --------------------------------------------- | | Максимум элементов | 100 | **5000** | | Обработка | Синхронная, результат в ответе | Асинхронная, результат через `/v1/batch/info` | | Управление ошибками | Нет (всегда «продолжай») | `on_error`: `continue` / `stop` | | Виды операций | Только выплаты | Платежи, возвраты, выплаты | | Статус | Легаси | **Рекомендуемый** | → [Массовые выплаты](/guides/mass-payouts) *** ## Практические правила * **Не поллите слишком часто.** `/v1/batch/info` — тоже запрос и тоже ест лимит частоты. Раз в несколько секунд более чем достаточно; вы только что сэкономили 5000 запросов, не тратьте их на поллинг. * **Проверьте баланс до батча выплат.** Если денег хватит на половину списка, вторая половина честно вернёт `insufficient_funds` в `items[].error` — но вы об этом узнаете только на разборе. * **Храните `batch_id` рядом с задачей.** Если ваш процесс упал в середине поллинга, `batch_id` — единственный способ забрать результаты потом. * **Уникальный `order_id` на каждый элемент.** Это ваш якорь для сверки: `idx` привязан к позиции в массиве, `order_id` — к вашей сущности. *** ## Связанные страницы `/v1/payout/mass` и когда он ещё уместен. Почему батч решает проблему `429`. # Счёт и чек на почту Source: https://docs.oblodai.com/guides/email-invoices Шлюз умеет сам писать покупателю письма — и вам для этого не нужен ни SMTP‑сервер, ни шаблоны, ни очередь рассылки. Есть два сценария, и они решают разные задачи. | Сценарий | Что делает | Кто инициирует | | -------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------- | | **Чек после оплаты** | Покупатель заплатил → ему автоматически уходит чек. | Шлюз, сам. Достаточно задать `payer_email` при создании платежа. | | **Счёт до оплаты** | Покупателю уходит письмо со счётом и кнопкой «Оплатить». | Вы, вызовом [`POST /v1/payment/send-email`](/reference/payment-send-email). | *** ## Автоматический чек после оплаты Это самое дешёвое улучшение вашей интеграции: **одно поле**. ```python Python theme={null} call("/v1/payment", { "amount": "10", "currency": "USD", "order_id": "order-1", "to_currency": "USDT", "network": "tron", "payer_email": "buyer@example.com", # ← этого достаточно }) ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "10", currency: "USD", order_id: "order-1", to_currency: "USDT", network: "tron", payer_email: "buyer@example.com", // ← этого достаточно }); ``` Когда платёж перейдёт в оплаченный статус, на этот адрес **автоматически** уйдёт чек. Ничего больше вызывать не нужно. **`payer_email` — это не просто справочное поле.** Раньше его описывали как «email плательщика», и многие передавали его «на всякий случай», не зная, что он включает отправку чека. Если вы **не** хотите, чтобы покупателю уходило письмо, — не заполняйте `payer_email`. Почему это стоит включить: криптоплатёж выглядит для покупателя тревожно (деньги ушли «в никуда», в блокчейн, отменить нельзя). Письмо‑чек с подтверждением снимает основную часть обращений в поддержку вида «я заплатил, вы получили?». *** ## Отправить счёт письмом Если вы хотите **выставить счёт** — то есть попросить оплатить, а не подтвердить оплату — используйте [`POST /v1/payment/send-email`](/reference/payment-send-email). Покупателю придёт письмо со счётом и кнопкой «Оплатить», ведущей на страницу оплаты. Платёж при этом должен уже существовать: письмо отправляется **по** платежу, а не вместо него. ```python Python theme={null} # 1. Создаём счёт как обычно inv = call("/v1/payment", { "amount": "250", "currency": "USD", "order_id": "invoice-42", "payer_email": "client@example.com", })["result"] # 2. Отправляем его клиенту письмом call("/v1/payment/send-email", {"order_id": "invoice-42"}) ``` ```js Node.js theme={null} // 1. Создаём счёт как обычно const inv = (await call("/v1/payment", { amount: "250", currency: "USD", order_id: "invoice-42", payer_email: "client@example.com", })).result; // 2. Отправляем его клиенту письмом await call("/v1/payment/send-email", { order_id: "invoice-42" }); ``` Идентифицировать платёж можно двумя способами — что удобнее: ```python Python theme={null} call("/v1/payment/send-email", {"uuid": inv["uuid"]}) # по uuid платежа call("/v1/payment/send-email", {"order_id": "invoice-42"}) # по вашему order_id ``` ```js Node.js theme={null} await call("/v1/payment/send-email", { uuid: inv.uuid }); // по uuid платежа await call("/v1/payment/send-email", { order_id: "invoice-42" }); // по вашему order_id ``` ### Отправить на другой адрес По умолчанию письмо уходит на `payer_email`, записанный в платеже. Чтобы отправить на другой адрес — передайте `email` явно: ```python Python theme={null} call("/v1/payment/send-email", { "order_id": "invoice-42", "email": "accounting@client.example", # например, в бухгалтерию клиента }) ``` ```js Node.js theme={null} await call("/v1/payment/send-email", { order_id: "invoice-42", email: "accounting@client.example", // например, в бухгалтерию клиента }); ``` Это же — способ **переслать счёт повторно**, если клиент говорит «письмо не пришло»: просто вызовите метод ещё раз. *** ## Типичный поток «выставил счёт → получил оплату» ``` 1. POST /v1/payment → создали счёт (payer_email = клиент) 2. POST /v1/payment/send-email → клиенту ушло письмо со счётом и кнопкой «Оплатить» 3. Клиент открывает письмо, жмёт кнопку, попадает на страницу оплаты, платит 4. Вам приходит вебхук invoice.paid 5. Клиенту АВТОМАТИЧЕСКИ уходит чек (потому что payer_email задан) ``` Шаг 5 — бесплатный бонус за то, что вы заполнили `payer_email` на шаге 1. **Счёт без своего сайта.** Если бэкенда нет вообще, тот же результат даёт [платёжная ссылка](/guides/payment-links): создали, отправили клиенту любым способом. А на странице оплаты покупатель может сам указать свой email — и чек ему уйдёт так же автоматически. *** ## Ошибки | Код | Значение | Что делать | | ------------------------ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `400 email.no_recipient` | Некому отправлять: в платеже нет `payer_email`, и `email` в запросе не передан. | Передайте `email` явно или задавайте `payer_email` при создании платежа. | | `503 email.disabled` | Отправка почты временно недоступна. | Временная ошибка — повторите с backoff. Платёж при этом жив, письмо можно отправить позже. | | `404 payment.not_found` | Платёж по `uuid`/`order_id` не найден. | Сверьте идентификатор. | *** ## Практические правила * **Валидируйте email на своей стороне.** Шлюз отправит письмо туда, что вы передали; опечатка в адресе = чек ушёл в никуда. * **Не считайте письмо доказательством оплаты.** Источник правды о статусе — вебхук `invoice.paid` и [`POST /v1/payment/info`](/reference/payment-info), а не факт отправки письма. * **`503 email.disabled` — не повод откатывать заказ.** Почта — вспомогательный канал; платёж от него не зависит. *** ## Связанные страницы поле `payer_email`. как узнать об оплате достоверно. # Сквозной пример приложения Source: https://docs.oblodai.com/guides/end-to-end-example Рабочий сервер целиком: от приёма платежа до обработки вебхука. Полностью рабочий пример: маленький сервер, который создаёт платёж, отдаёт покупателю ссылку на оплату и принимает вебхук о поступлении, помечая заказ оплаченным. Это тот же поток, что в [Быстром старте](/quickstart), но собранный в один запускаемый файл. Дальше — две версии: **Python (Flask)** и **Node.js (Express)**. Логика одинаковая. *** ## Что делает пример ``` POST /checkout → создаёт счёт в Oblodai, возвращает ссылку на оплату POST /oblodai/callback → принимает вебхук, проверяет подпись, помечает заказ оплаченным GET /order/ → показывает статус заказа (для наглядности) ``` Хранилище заказов — простой словарь в памяти (в реальном приложении замените на БД). *** ## Код примера ```python app.py expandable theme={null} # app.py — минимальный сквозной пример приёма платежей Oblodai import hmac, hashlib, time, json, os import requests from flask import Flask, request, jsonify # --- Конфигурация (в проде — из секрет-хранилища, не из кода) --- SECRET = os.environ["OBLODAI_SECRET"].encode() # секрет API-ключа PUBLIC_ID = os.environ["OBLODAI_PUBLIC_ID"] WEBHOOK_SECRET = os.environ["OBLODAI_WEBHOOK_SECRET"].encode() # секрет из /v1/webhooks BASE = "https://api.oblodai.com" app = Flask(__name__) ORDERS = {} # order_id -> {"status": ..., "amount": ...} (замените на БД) SEEN = set() # (uuid, status) — дедупликация вебхуков (замените на БД) # --- Подписанный вызов API --- def call(path: str, payload: dict, idempotency_key: str | None = None) -> dict: body = json.dumps(payload, separators=(",", ":")) ts = str(int(time.time())) signing = f"{ts}\nPOST\n{path}\n{body}" sig = hmac.new(SECRET, signing.encode(), hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, } if idempotency_key: # Защита от дублей при ретраях. В подпись НЕ входит. headers["Idempotency-Key"] = idempotency_key r = requests.post(BASE + path, data=body, headers=headers, timeout=30) return r.json() # --- 1. Оформление заказа: создаём счёт --- @app.post("/checkout") def checkout(): data = request.get_json() order_id = data["order_id"] # ваш идентификатор заказа amount = data["amount"] # сумма в USD resp = call("/v1/payment", { "amount": amount, "currency": "USD", # валюта ЦЕНЫ: USD, EUR, RUB… (23 фиата) или монета "order_id": order_id, # ваш бизнес-ключ: тоже дедуплицирует "to_currency": "USDT", "network": "tron", "lifetime": 3600, }, idempotency_key=f"checkout-{order_id}") # один ключ на действие, стабилен при ретраях if "error" in resp: return jsonify({"error": resp["error"]}), 400 inv = resp["result"] ORDERS[order_id] = {"status": "pending", "amount": amount, "uuid": inv["uuid"]} # Покупателю отдаём ссылку на hosted-страницу оплаты return jsonify({"pay_url": inv["url"], "address": inv["address"]}) # --- 2. Вебхук о поступлении --- @app.post("/oblodai/callback") def callback(): raw = request.get_data() # СЫРОЕ тело — не пересериализовывать event = request.get_json(silent=True) or {} # Пробные тела не подписаны — просто подтверждаем приём if event.get("is_test"): return "ok", 200 ts = request.headers.get("X-Webhook-Timestamp", "") sig = request.headers.get("X-Webhook-Signature", "") expected = hmac.new(WEBHOOK_SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): return "bad signature", 403 # Идемпотентность: один вебхук может прийти несколько раз key = (event.get("uuid"), event.get("status")) if key in SEEN: return "ok", 200 # уже обработано — no-op SEEN.add(key) # Реагируем на статус if event.get("type") == "payment" and event.get("status") == "paid": order_id = event.get("order_id") if order_id in ORDERS: ORDERS[order_id]["status"] = "paid" # ... здесь выдаём товар/услугу (тоже идемпотентно) return "ok", 200 # 2xx только после успешной обработки # --- 3. Статус заказа (для наглядности) --- @app.get("/order/") def order(order_id): return jsonify(ORDERS.get(order_id, {"status": "unknown"})) if __name__ == "__main__": app.run(port=8000) ``` #### Запуск ```bash theme={null} pip install flask requests export OBLODAI_PUBLIC_ID='oblodai_…' export OBLODAI_SECRET='oblodai_live_…' export OBLODAI_WEBHOOK_SECRET='b7c1e9…' # из POST /v1/webhooks python app.py ``` Проверка: ```bash theme={null} # создать заказ curl -s localhost:8000/checkout -X POST -H 'Content-Type: application/json' \ -d '{"order_id":"test-001","amount":"1"}' # → {"pay_url": "...", "address": "..."} # посмотреть статус curl -s localhost:8000/order/test-001 ``` ```js app.js expandable theme={null} // app.js — минимальный сквозной пример приёма платежей Oblodai import express from "express"; import crypto from "node:crypto"; const SECRET = process.env.OBLODAI_SECRET; const PUBLIC_ID = process.env.OBLODAI_PUBLIC_ID; const WEBHOOK_SECRET = process.env.OBLODAI_WEBHOOK_SECRET; const BASE = "https://api.oblodai.com"; const app = express(); const ORDERS = new Map(); // order_id -> {...} (замените на БД) const SEEN = new Set(); // "uuid:status" — дедуп (замените на БД) // Подписанный вызов API async function call(path, payload, idempotencyKey = null) { const body = JSON.stringify(payload); const ts = Math.floor(Date.now() / 1000).toString(); const signing = `${ts}\nPOST\n${path}\n${body}`; const sig = crypto.createHmac("sha256", SECRET).update(signing).digest("hex"); const headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, }; if (idempotencyKey) { // Защита от дублей при ретраях. В подпись НЕ входит. headers["Idempotency-Key"] = idempotencyKey; } const res = await fetch(BASE + path, { method: "POST", headers, body }); return res.json(); } // 1. Оформление заказа app.post("/checkout", express.json(), async (req, res) => { const { order_id, amount } = req.body; const resp = await call("/v1/payment", { amount, currency: "USD", order_id, to_currency: "USDT", network: "tron", lifetime: 3600, }, `checkout-${order_id}`); // один ключ на действие, стабилен при ретраях if (resp.error) return res.status(400).json({ error: resp.error }); const inv = resp.result; ORDERS.set(order_id, { status: "pending", amount, uuid: inv.uuid }); res.json({ pay_url: inv.url, address: inv.address }); }); // 2. Вебхук — ВАЖНО: сырое тело, не express.json() app.post("/oblodai/callback", express.raw({ type: "*/*" }), (req, res) => { const raw = req.body.toString("utf8"); let event = {}; try { event = JSON.parse(raw); } catch { /* ignore */ } if (event.is_test) return res.send("ok"); // пробное тело не подписано const ts = req.get("X-Webhook-Timestamp") || ""; const sig = req.get("X-Webhook-Signature") || ""; const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(`${ts}.${raw}`).digest("hex"); const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)); if (!ok) { return res.status(403).send("bad signature"); } const key = `${event.uuid}:${event.status}`; if (SEEN.has(key)) return res.send("ok"); // дедуп SEEN.add(key); if (event.type === "payment" && event.status === "paid") { const o = ORDERS.get(event.order_id); if (o) o.status = "paid"; // выдать товар (идемпотентно) } res.send("ok"); }); // 3. Статус заказа app.get("/order/:id", (req, res) => { res.json(ORDERS.get(req.params.id) || { status: "unknown" }); }); app.listen(8000, () => console.log("on :8000")); ``` #### Запуск Пример написан на ESM (`import`) — в `package.json` нужен `"type": "module"` (либо назовите файл `app.mjs`): ```bash theme={null} npm init -y && npm pkg set type=module npm install express export OBLODAI_PUBLIC_ID='oblodai_…' export OBLODAI_SECRET='oblodai_live_…' export OBLODAI_WEBHOOK_SECRET='b7c1e9…' node app.js # Node.js ≥ 20 — используется глобальный fetch ``` ## На что обратить внимание в примере * **Сырое тело для вебхука.** И во Flask (`request.get_data()`), и в Express (`express.raw`) подпись проверяется по **сырым** байтам. `express.json()` на вебхук‑роуте всё сломает. * **Два разных секрета.** `SECRET` — для подписи исходящих запросов; `WEBHOOK_SECRET` — для проверки входящих вебхуков. Это разные значения из разных источников. * **Дедупликация.** `SEEN` защищает от повторной выдачи товара — вебхук может прийти несколько раз. * **`2xx` в конце.** Отвечаем `ok` только после обработки; при ошибке вернули бы не‑2xx, чтобы Oblodai повторил доставку. * **В памяти — только для примера.** `ORDERS`/`SEEN` замените на БД, иначе после перезапуска данные и дедуп‑ключи потеряются. *** ## Что дальше * [Тестирование интеграции](/guides/testing) — как прогнать этот пример без реальных переводов. * [Настройка вебхуков](/guides/webhooks-setup) — детали проверки подписи и идемпотентности. * [Чек‑лист перед запуском](/guides/production-checklist) — перед боевым трафиком. *** ## Связанные страницы # FAQ и устранение неполадок Source: https://docs.oblodai.com/guides/faq-troubleshooting Частые вопросы — короткие ответы. Нужна **пошаговая диагностика** по симптому (деревья «платёж застрял», «вебхук не приходит», таймауты) — на странице [Что делать, если не работает](/guides/troubleshooting). Полный список кодов ошибок — [Справочник кодов ошибок](/reference/errors-catalog). *** ## Аутентификация и подпись Почти всегда причина одна: **подписали одну строку тела, а отправили другую**. Библиотека могла переупорядочить поля JSON или добавить пробелы, и байты тела перестали совпадать с подписанными. **Что проверить:** 1. Сериализуйте тело **один раз** в переменную и используйте её и для подписи, и для отправки. 2. Каноническая строка — ровно `timestamp\nPOST\npath\nbody`, с переводами строк `\n`. 3. `path` — с ведущим слэшем (`/v1/payment`), без базового URL. 4. Подпись — hex в нижнем регистре. → [Как подписать запрос](/guides/signing-requests) `X-Timestamp` **отсутствует или не число**. Передавайте unix‑секунды (целое), не миллисекунды. Если часы разошлись больше чем на 5 минут — код будет **`merchant.bad_signature`** (проверка окна ±5 мин встроена в сверку подписи), а не `auth.bad_timestamp`. Синхронизируйте время (NTP). У вас включён [IP‑allowlist](/reference/api-allowlist), а запрос идёт с адреса вне списка. Либо добавьте IP вашего backend, либо временно выключите контроль. Это **разные алгоритмы**. Подпись запроса — `timestamp\nMETHOD\npath\nbody`. Подпись вебхука — `timestamp.сырое_тело` (точка‑разделитель, без метода и пути), другим секретом. → [Объект вебхука](/reference/webhook-object) ## Платежи Скорее всего это **валюто‑агностичный счёт** (`is_multi: true`): вы не задали `to_currency`/`network`, и адрес выделится только после того, как покупатель выберет валюту на hosted‑странице через [`/v1/pay/{id}/select`](/reference/pay-select). Если адрес нужен сразу — задайте валюту и сеть при создании. → [Три режима создания счёта](/guides/payment-modes) У валюты несколько сетей (например, у `USDT` их много), а `network` вы не указали. Укажите сеть явно или используйте валюту с единственной сетью. → [Поддерживаемые сети](/reference/basics-networks) Сумма ниже минимума сети. На дорогих сетях (Ethereum, Bitcoin) минимум есть. Увеличьте сумму или используйте дешёвую сеть (Tron, Polygon, BSC). Проверьте промежуточные статусы: `confirm_check` означает, что транзакцию видят и **ждут подтверждений** сети. Число подтверждений зависит от суммы и сети — крупный платёж ждёт дольше. Пока не достигнут порог, статус не станет `paid`. → [Приём первого платежа](/guides/accept-first-payment) Зависит от настроек. Если сумма в пределах [допуска](/reference/payment-accuracy) — счёт всё равно `paid`. Если нет и срок вышел — `wrong_amount`, и средства вернутся при включённом [автовозврате](/reference/payment-autorefund). → [Недоплата и переплата](/guides/under-overpayment) ## Вебхуки Пройдите по списку: 1. **Endpoint зарегистрирован?** Вызовите [`/v1/webhooks`](/reference/webhooks-register) и сохраните секрет. Без регистрации доставок нет вовсе — даже per‑объектный `url_callback` игнорируется (без секрета endpoint подписывать нечем, доставка не создаётся). 2. **URL публично доступен по HTTPS?** Приватные/локальные адреса запрещены SSRF‑проверкой. 3. **Отправьте пробное событие** и посмотрите `status_code`. → [Отладка вебхуков](/guides/webhooks-debug) 4. **Проверьте журнал доставок** — статус `pending`/`dead` и `last_error` подскажут причину. → [`/v1/webhooks/deliveries`](/reference/webhooks-deliveries) * Берёте ли вы **сырое тело** (до JSON‑парсинга)? Пересериализация меняет байты. * Правильный ли алгоритм: `hex(HMAC-SHA256(secret, "{timestamp}." + сырое_тело))`? * Тот ли секрет — из [`/v1/webhooks`](/reference/webhooks-register), а не ключ API? * Пробные тела (`is_test: true`) **не подписаны** — их проверять подписью не нужно. Это нормально: доставка **at‑least‑once**. Обязательно дедуплицируйте по `uuid` + `status` и обрабатывайте повтор как no‑op. → [Настройка вебхуков](/guides/webhooks-setup#обрабатывать-идемпотентно) Вы вернули не‑`2xx` (или запрос упал по таймауту). Диспетчер считает это провалом и повторяет. Отвечайте `2xx` **только после** успешной обработки; если обработка не удалась — тогда да, пусть повторит. ## Выплаты и баланс Возможны **незрелые средства**: депозит на балансе виден, но ещё не набрал подтверждений и невыводим. Выводимый остаток может быть меньше показанного в [`/v1/balance`](/reference/balance). Дождитесь подтверждений. Отдельный код для этого случая — `409 payout.funds_maturing`. Средства ещё дозревают (maturity‑холд для reorg‑безопасности). Это временно — повторите позже с backoff или выведите уже зрелую часть. Вы указали адрес, принадлежащий самому шлюзу (self‑dealing запрещён). Укажите внешний адрес. Для выплаты `order_id` **обязателен** — это ваш бизнес‑ключ дедупликации. Всегда задавайте уникальный. Он дополняет (а не заменяет) HTTP‑заголовок `Idempotency-Key`. → [Устойчивый клиент](/guides/resilient-client#главный-принцип) Это ожидаемо: элементы независимы. Разбирайте результат **поэлементно** — у неуспешных виден признак неудачи и причина. В [батче](/guides/batch-operations) это `items[].status` + `items[].error`, в легаси `/v1/payout/mass` — `success: false` + `message`. → [Массовые операции](/guides/batch-operations) Используйте **батч** [`POST /v1/payout/batch`](/reference/payout-batch): до **5000** выплат **одним** подписанным запросом, обработка асинхронная, результат — через `/v1/batch/info`. Цикл из отдельных вызовов гарантированно упрётся в лимит частоты. → [Массовые операции](/guides/batch-operations) ## Общее Это временные ошибки. Повторяйте с **экспоненциальным backoff**. Благодаря идемпотентности повтор денег‑движущих операций безопасен (дубля не будет) — при условии, что вы повторяете **тот же** запрос: с тем же HTTP‑заголовком `Idempotency-Key` и тем же `order_id`. → [Идемпотентность](/reference/basics-idempotency) Это HTTP‑заголовок с любым уникальным значением (до 255 символов), который вы генерируете **до первой отправки** и не меняете при ретраях. Повтор с тем же ключом вернёт **тот же ответ** и заголовок `Idempotent-Replayed: true` вместо создания дубля. Формально не обязателен, но настоятельно рекомендуется. Второй, независимый механизм защиты — ваш `order_id` (для выплат он **обязателен**). Опасен только случай, когда нет ни того, ни другого: тогда повтор создаст дубль. → [Устойчивый клиент](/guides/resilient-client#главный-принцип) **Нет.** Не все `409` финальны: * `payout.funds_maturing` — средства дозревают, **повторите позже**, выплата пройдёт; * `idempotency.in_progress` — ваш прежний запрос ещё выполняется, **подождите и повторите**; * `payout.frozen` — временная защита, **повторите позже**; * `payout.insufficient_funds` — вот это по‑настоящему «денег нет», повтор не поможет. → [Устойчивый клиент](/guides/resilient-client#не-все-409-финальны) Превышена частота запросов. Снизьте темп и повторяйте с backoff. → [Ограничение частоты](/reference/basics-ratelimit) Песочницы действительно нет. Большую часть логики можно проверить без переводов (подпись, создание счёта, тестовые вебхуки), остальное — малыми суммами на дешёвой сети. → [Тестирование интеграции](/guides/testing) Только в личном кабинете под 2FA — в публичном API ротации нет. Заморозки выплат при ротации нет. → [Безопасность в проде](/guides/production-security#3-ротация-ключа) Загляните в [Справочник кодов ошибок](/reference/errors-catalog) и [Глоссарий](/reference/glossary). Если проблема не про конкретный код, а про поведение — сверьтесь со страницей нужного метода в [Справочнике](/reference/overview). Если и это не помогло — создайте тикет в поддержку в кабинете [my.oblodai.com](https://my.oblodai.com) (раздел **«Поддержка»**). Приложите ваш `public_id`, `uuid` платежа/выплаты и строки лога с префиксом `oblodai:` (в SDK включаются переменной `OBLODAI_LOG`, в модулях CMS — галочкой **Debug log**); `secret` **не** прикладывайте. ## Связанные страницы # Первая выплата Source: https://docs.oblodai.com/guides/first-payout Выплата — это вывод средств с вашего баланса на внешний адрес. По API‑ключу выплата **авто‑одобряется** и уходит сразу, без белых списков и периодов выдержки. Разберём поток от проверки баланса до подтверждающего вебхука. Справочник метода — [`POST /v1/payout`](/reference/payout-create). *** ## Поток ``` 1. Проверяете баланс (POST /v1/balance) 2. (опц.) Считаете комиссию заранее (POST /v1/payout/calculate) 3. Создаёте выплату (POST /v1/payout) — уходит сразу 4. Ловите вебхук payout.* о подтверждении ``` *** ```python Python theme={null} bal = call("/v1/balance", {})["result"]["balance"]["merchant"] # [{"currency": "USDT", "balance": "1240.75"}, ...] ``` ```js Node.js theme={null} const bal = (await call("/v1/balance", {})).result.balance.merchant; // [{"currency": "USDT", "balance": "1240.75"}, ...] ``` **Осторожно с незрелыми средствами.** Свежий депозит виден в балансе, но до набора подтверждений находится под maturity‑холдом и **не выводим**. Выплата на сумму, включающую незрелые средства, вернёт `409 payout.funds_maturing`. Выводимый остаток может быть временно меньше показанного. См. [`POST /v1/balance`](/reference/balance). Чтобы заранее понять, сколько уйдёт с баланса и сколько получит адрес: ```python Python theme={null} calc = call("/v1/payout/calculate", { "amount": "25", "currency": "USDT", "network": "tron", "is_subtract": False, })["result"] # {"commission": "1.1", "merchant_amount": "25", "to_amount": "23.9"} ``` ```js Node.js theme={null} const calc = (await call("/v1/payout/calculate", { amount: "25", currency: "USDT", network: "tron", is_subtract: false, })).result; // {"commission": "1.1", "merchant_amount": "25", "to_amount": "23.9"} ``` * `is_subtract: false` — комиссия **из** суммы (получатель получает меньше). * `is_subtract: true` — комиссия **сверх** суммы (списывается с баланса дополнительно). `is_subtract` здесь — параметр **предрасчёта** `calculate`. На самой выплате `/v1/payout` это поле не действует: кто платит сетевую комиссию, определяет настройка проекта [`fee-config`](/reference/payout-fee-config). Считайте `calculate` с тем же вариантом, что стоит в fee‑config, — тогда превью совпадёт с реальной выплатой. ```python Python theme={null} import uuid key = str(uuid.uuid4()) # Idempotency-Key: ОДИН на действие, не меняется при ретраях payout = call("/v1/payout", { "amount": "25", "currency": "USDT", "network": "tron", # обязательна для монет с несколькими сетями "address": "TXY...", "order_id": "payout-1", # ОБЯЗАТЕЛЬНО для выплаты — ваш бизнес-ключ }, idempotency_key=key)["result"] print(payout["status"]) # "process" — уже отправляется print(payout["approval_required"]) # false — подтверждение не нужно ``` ```js Node.js theme={null} import { randomUUID } from "node:crypto"; const key = randomUUID(); // Idempotency-Key: ОДИН на действие, не меняется при ретраях const payout = (await call("/v1/payout", { amount: "25", currency: "USDT", network: "tron", // обязательна для монет с несколькими сетями address: "TXY...", order_id: "payout-1", // ОБЯЗАТЕЛЬНО для выплаты — ваш бизнес-ключ }, key)).result; // key → заголовок Idempotency-Key в вашей обёртке call() console.log(payout.status); // "process" — уже отправляется console.log(payout.approval_required); // false — подтверждение не нужно ``` Выплата — самая дорогая операция для дубля, поэтому здесь работают **оба** механизма защиты: * **`Idempotency-Key`** (HTTP‑заголовок) — генерируется **до первой отправки** и одинаков во всех ретраях. Повтор с тем же ключом вернёт **тот же ответ** и заголовок `Idempotent-Replayed: true`. Если предыдущая попытка ещё выполняется — придёт `409 idempotency.in_progress`: подождите и повторите. * **`order_id`** — для выплаты **обязателен** (без него `400 payout.order_id_required`). Повтор с тем же `order_id` вернёт уже созданную выплату, а не отправит вторую. Как добавить заголовок в свою обёртку `call()` — [Устойчивый клиент](/guides/resilient-client#главный-принцип). Статус выплаты в ответах — укрупнённый ([маппинг](/reference/payout-object#статусы-выплаты)): | `status` | Значение | | --------- | --------------------------------------------------------- | | `check` | Создана, средства зарезервированы, ждёт одобрения. | | `process` | Одобрена / отправляется / отправлена, ждёт подтверждений. | | `paid` | Подтверждена в блокчейне — готово. | | `fail` | Отклонена / отправка не удалась, резерв освобождён. | | `cancel` | Отменена до отправки, резерв освобождён. | Финал — `is_final: true` (`paid`/`fail`/`cancel`). О смене статуса приходит вебхук `payout.*` — ловите его так же, как платёжный (проверка подписи, идемпотентность). См. [Настройка вебхуков](/guides/webhooks-setup). *** ## Частые ошибки | Код | Что значит | Что делать | | --------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | | `409 payout.insufficient_funds` | Недостаточно доступного баланса. | Пополнить баланс или уменьшить сумму. Повтор не поможет. | | `409 payout.funds_maturing` | Средства ещё дозревают. | **Повторить позже** — это временная ошибка, а не отказ. Или вывести меньшую сумму (без свежих депозитов). | | `409 idempotency.in_progress` | Запрос с этим `Idempotency-Key` ещё выполняется. | **Подождать и повторить** — получите результат первой попытки. | | `400 payout.destination_internal` | Адрес принадлежит шлюзу. | Указать внешний адрес. | | `400 payout.order_id_required` | Не передан `order_id`. | Всегда задавайте `order_id`. | | `400 payout.amount_below_fee` | Сумма меньше комиссии сети. | Увеличить сумму. | **`409` ≠ «всё, выплата отменена».** Не бросайте выплату при первом же `409`: `payout.funds_maturing` и `idempotency.in_progress` **надо повторить позже**, и деньги уйдут. Разбор — [Устойчивый клиент](/guides/resilient-client#не-все-409-финальны). *** ## Важно про безопасность Выплаты по API‑ключу уходят **сразу** и **необратимы**. Ответственность за адрес назначения — на вашей стороне. Рекомендуется: * Включить [IP‑allowlist](/reference/api-allowlist), чтобы выплаты можно было инициировать только с вашего backend. * Хранить `secret` только на сервере. * Валидировать адрес получателя на своей стороне до вызова. См. [Безопасность в проде](/guides/production-security). *** ## Связанные страницы # Регистрация и ключи — с чего начать Source: https://docs.oblodai.com/guides/get-keys Заведите аккаунт и получите public_id и secret для работы с API. Прежде чем принимать платежи, нужно завести аккаунт и получить **API‑ключ**. Это самый первый шаг — без ключа ничего не заработает. *** Откройте личный кабинет Oblodai и зарегистрируйтесь: **Кабинет:** [https://my.oblodai.com](https://my.oblodai.com) Там же — вход, если аккаунт уже есть. В кабинете откройте раздел **«API‑ключи»** в боковом меню ([my.oblodai.com](https://my.oblodai.com) → «API‑ключи»). Отдельной кнопки «создать ключ» нет: ключ появляется автоматически вместе с мерчантом (раздел «Мерчанты» → «+ Создать мерчанта»). Секрет показывается **один раз** — при создании мерчанта или при нажатии «Обновить ключ» на странице «API‑ключи». Вы получите **пару**: * **`public_id`** — несекретный идентификатор. Его можно логировать; он уходит в заголовке `X-Public-Id`. * **`secret`** — секрет для подписи запросов. **Показывается один раз** — сразу сохраните его в надёжном месте (менеджер секретов, переменные окружения). Потеряли — перевыпустите ключ. **Один ключ — на всё.** Одна пара ключей работает и для приёма платежей, и для выплат, и для регистрации вебхуков. Отдельные ключи заводить не нужно. Технические детали подписи — [Аутентификация](/reference/basics-auth). Способ интеграции зависит от того, что у вас есть: | У вас… | Вам нужно | Куда идти | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------- | | **Свой сайт/бэкенд** (можете писать серверный код) | Сервер, где будет считаться подпись, и публичный HTTPS‑адрес для вебхуков | [SDK для вашего языка](/sdk/overview) | | Только **сайт на конструкторе** (Tilda/Wix и т. п.), **без сервера** | Ничего программировать не нужно: создайте **платёжную ссылку** и повесьте её на кнопку «Оплатить» | [Платёжные ссылки](/guides/payment-links) | | Вообще **нет сайта** (продаёте в мессенджере, соцсети, принимаете донаты) | Тоже платёжная ссылка — её можно просто отправить покупателю | [Платёжные ссылки](/guides/payment-links) | **Сервер нужен не всегда.** Секрет ключа подписывает запросы и **никогда** не должен попадать в браузер — поэтому подпись считается только на сервере. Но если своего сервера нет, это **не** тупик: **[платёжная ссылка](/guides/payment-links)** создаётся один раз (из кабинета или одним подписанным вызовом), после чего работает сама. Покупатель открывает её, вводит сумму (если ссылка это позволяет), выбирает монету и платит — ваш код в этом не участвует вообще, поэтому секрету и негде утечь. Что при этом теряется: без бэкенда некому принять вебхук, поэтому автоматически выдавать товар/доступ будет нечему. Оплаты вы видите в кабинете и через [`POST /v1/payment/link/info`](/reference/payment-link). Для донатов, предоплат и разовых счетов этого достаточно; для интернет‑магазина с автовыдачей всё же нужен модуль CMS или бэкенд. *** ## Важно: песочницы нет — платежи настоящие У Oblodai **нет тестового режима и тестовых ключей**. Любой платёж двигает **реальные** средства в блокчейне. Это значит: * «Тестовый» платёж на старте — это **настоящая** оплата на маленькую сумму (например, эквивалент \$1–2), которую вы отправляете сами себе. * Что можно проверить **без** реальных переводов (создание счёта, подпись, приём тестового вебхука) — описано в разделе [Тестирование](/guides/testing). Прочитайте его перед первым платежом. *** ## Сколько это стоит Комиссия за **приём** платежа — по умолчанию **1.5 % + \$0.30 с каждого платежа** (у реферала 1.4 %; со статических кошельков — та же ставка, но без фиксированных \$0.30). Она удерживается **при зачислении**: покупатель заплатил \$100 → на баланс придёт ≈ \$98.2. Сетевая комиссия (газ) при **выплате** — отдельная величина; по умолчанию её платит получатель. Подробно и с примером — [Модель баланса и движение средств](/guides/balance-and-funds#комиссии-кто-и-за-что-платит). *** ## Куда дальше * Пишете код → [SDK](/sdk/overview), затем [Быстрый старт](/quickstart). * Хотите вручную по HTTP → [Быстрый старт](/quickstart) и [Как подписать запрос](/guides/signing-requests). * Нет сервера (конструктор сайта, соцсети, донаты) → [Платёжные ссылки](/guides/payment-links). # Массовые выплаты Source: https://docs.oblodai.com/guides/mass-payouts Когда нужно выплатить многим получателям сразу, есть два пути. Выбор между ними — не вопрос вкуса: неправильный упирается в лимит частоты запросов. | | [`POST /v1/payout/batch`](/reference/payout-batch) | [`POST /v1/payout/mass`](/reference/payout-mass) | | ------------------- | -------------------------------------------------- | ------------------------------------------------ | | Максимум за раз | **5000** | 100 | | Обработка | Асинхронная (`batch_id` + поллинг) | Синхронная (результат сразу в ответе) | | Управление ошибками | `on_error`: `continue` / `stop` | Нет | | Статус | **Рекомендуемый путь** | **Легаси** | **Не нарезайте большой список на пачки по 100.** Раньше эта страница советовала именно так — и это прямой путь в `429 rate limit`: 5000 выплат = 50 запросов подряд, а лимит частоты считается по запросам (по умолчанию 120/мин на IP). Правильный ответ на «много выплат» — **батч**: один подписанный запрос на всю пачку. → [Массовые операции](/guides/batch-operations) *** ## Большие списки: батч Для любого сколько‑нибудь серьёзного объёма используйте [`POST /v1/payout/batch`](/reference/payout-batch): ```python Python theme={null} resp = call("/v1/payout/batch", { "payouts": [ {"amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "p-1"}, {"amount": "10", "currency": "USDT", "network": "tron", "address": "TZZ...", "order_id": "p-2"}, # … до 5000 элементов ], "on_error": "continue", })["result"] batch_id = resp["batch_id"] # результаты забираются позже, через /v1/batch/info ``` ```js Node.js theme={null} const resp = (await call("/v1/payout/batch", { payouts: [ { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "p-1" }, { amount: "10", currency: "USDT", network: "tron", address: "TZZ...", order_id: "p-2" }, // … до 5000 элементов ], on_error: "continue", })).result; const batchId = resp.batch_id; // результаты забираются позже, через /v1/batch/info ``` Полный цикл (отправка → поллинг → поэлементный разбор), `on_error`, ошибки и практические правила — на отдельной странице: **[Массовые операции](/guides/batch-operations)**. *** ## Маленькие пачки: легаси `/v1/payout/mass` `/v1/payout/mass` (до 100 выплат) никуда не делся и продолжает работать. Он оправдан ровно в одном случае: **пачка маленькая, и вам нужен результат прямо в ответе**, без поллинга. Если сомневаетесь — берите батч. ### Ключевая идея: частичный успех — это норма Массовая выплата **не** работает по принципу «весь список прошёл или весь откатился». Каждый элемент живёт своей жизнью: у одного может не хватить баланса, у другого — некорректный адрес, а остальные уйдут нормально. Поэтому результат нужно разбирать **поэлементно**. (В батче это правило то же самое.) ### Пример ```python Python theme={null} resp = call("/v1/payout/mass", {"payouts": [ {"amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "p-1"}, {"amount": "10", "currency": "USDT", "network": "tron", "address": "TZZ...", "order_id": "p-2"}, {"amount": "5", "currency": "USDT", "network": "tron", "address": "TWW...", "order_id": "p-3"}, ]}) for item in resp["result"]["items"]: if item["success"]: print(item["order_id"], "→", item["status"]) # ушла в обработку else: print(item["order_id"], "ОШИБКА:", item["message"]) # не создана, причина в message ``` ```js Node.js theme={null} const resp = await call("/v1/payout/mass", { payouts: [ { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "p-1" }, { amount: "10", currency: "USDT", network: "tron", address: "TZZ...", order_id: "p-2" }, { amount: "5", currency: "USDT", network: "tron", address: "TWW...", order_id: "p-3" }, ]}); for (const item of resp.result.items) { if (item.success) { console.log(item.order_id, "→", item.status); // ушла в обработку } else { console.log(item.order_id, "ОШИБКА:", item.message); // не создана, причина в message } } ``` Поля каждого элемента `payouts[]` — те же, что у [`POST /v1/payout`](/reference/payout-create): `amount`, `currency`, `address`, `order_id` (обязателен), опционально `network`, `memo`, `url_callback` и другие. ### Ответ ```json theme={null} { "state": 0, "result": { "items": [ { "uuid": "…", "order_id": "p-1", "status": "process", "is_final": false, "approval_required": false, "success": true }, { "order_id": "p-2", "success": false, "message": "available balance is less than the requested amount" } ] } } ``` * **Успешный** элемент: `uuid`, `status`, `success: true`. * **Неуспешный** элемент: `success: false` и текст причины в `message`. ### Ошибки уровня запроса Если отклонён **весь** запрос (а не отдельные элементы): | Код | Значение | | ---------------------------- | ------------------------------------------------------------------------------ | | `400 request.bad_json` | Тело не парсится. | | `400 payout.empty_batch` | Пустой массив `payouts`. | | `400 payout.batch_too_large` | Больше 100 элементов → это сигнал перейти на [батч](/guides/batch-operations). | *** ## Рекомендации (для обоих путей) * **Больше 100 выплат — только батч.** Не нарезайте на пачки: это лишние запросы и `429`. * **Уникальный `order_id` на каждый элемент.** Каждая выплата идемпотентна по своему `order_id` — при повторе списка уже отправленные не уйдут дважды. * **`Idempotency-Key` на сам запрос.** Защищает от «отправил список, ответ потерялся, отправил ещё раз». Особенно важно для батча: дубль из 5000 выплат — очень дорогая ошибка. → [Устойчивый клиент](/guides/resilient-client#главный-принцип) * **Сохраняйте `uuid` успешных элементов** и сверяйте финальный статус через [`POST /v1/payout/info`](/reference/payout-info) или по вебхукам `payout.*`. * **Проверьте баланс заранее** — при нехватке средств часть элементов вернёт `insufficient_funds`. * **Разбирайте результат поэлементно.** Частичный успех — норма, а не сбой. *** ## Связанные страницы батчи платежей, возвратов и выплат (до 5000). почему пачки по 100 упираются в лимит. # Переход с Heleket на Oblodai Source: https://docs.oblodai.com/guides/migration-heleket **Не мигрируете с Heleket?** Смело пропускайте эту страницу — здесь только отличия при переносе существующей интеграции с Heleket, а не общие правила Oblodai. Oblodai намеренно держит совместимость с Heleket в ряде мест (например, укрупнённые статусы выплат), но некоторые вещи отличаются. Эта инструкция собирает отличия, о которых важно знать при переносе интеграции. Список отличий основан на явно задокументированных расхождениях. Всегда сверяйтесь с актуальным [Справочником](/reference/overview) по каждому методу. *** ## Аутентификация Oblodai подписывает **каждый** запрос HMAC‑SHA256 по канонической строке `timestamp\nMETHOD\npath\nbody` и тремя заголовками `X-Public-Id` / `X-Timestamp` / `X-Signature`. Проверьте, что ваша реализация подписи собирает строку именно так и подписывает **те же байты**, что отправляет. → [Как подписать запрос](/guides/signing-requests) *** ## Курсы валют Метод: `GET /v1/exchange-rate/{currency}/list` → **`POST /v1/exchange-rate/list`** | | Heleket | Oblodai | | ----------- | ------- | --------------------------------------------- | | Котировка к | USD | **USDT** | | Фильтр | в пути | тело `{ "currency_from": "…" }` (опционально) | Неизвестная валюта возвращает пустой `result: []`, а не ошибку. → [`POST /v1/exchange-rate/list`](/reference/exchange-rate-list) *** ## Формат ответа Все ответы (кроме [`/v1/webhooks`](/reference/webhooks-register) и `429` — он отдаёт особое тело `{ "state": 1, "message": … }` без объекта `error`) — конверт `{ "state": 0, "result": … }` при успехе или `{ "error": { "code", "message" } }` при ошибке. Ветвитесь по `error.code` вида `<домен>.<причина>`. → [Формат ответа и коды ошибок](/reference/basics-errors) **Исключение:** [`POST /v1/webhooks`](/reference/webhooks-register) отвечает `201 Created` без конверта. *** ## Статусы выплат Внутренний жизненный цикл выплаты — `pending → approved → sent → confirmed` (либо `failed`/ `cancelled`), но в поле `status` приходит **укрупнённый Heleket‑совместимый** статус: | `status` в ответе | Значение | | ----------------- | -------------------------------- | | `check` | создана, ждёт одобрения | | `process` | одобрена/отправляется/отправлена | | `paid` | подтверждена | | `fail` | отклонена/не удалась | | `cancel` | отменена | Здесь совместимость сохранена — но проверьте, что вы не полагаетесь на внутренние промежуточные статусы. → [Объект выплаты](/reference/payout-object) *** ## Подпись вебхуков Убедитесь, что проверка подписи вебхука использует алгоритм Oblodai: `hex(HMAC-SHA256(secret, "{timestamp}." + сырое_тело))`, где секрет — из [`/v1/webhooks`](/reference/webhooks-register). Это **не** тот же алгоритм, что подпись запроса. → [Объект вебхука](/reference/webhook-object) *** ## Поддерживаемые сети — чего нет Проверьте, что вы не шлёте в Oblodai сети, которых здесь нет: * **Bitcoin‑семейство — только `BTC`.** `litecoin`, `dogecoin`, `bitcoincash`, `dash` намеренно **не поддерживаются** (как и `monero`) — инвойс/кошелёк на такую сеть будет отклонён. * **Нативный ETH — только на `ethereum`.** На L2 (`base`, `arbitrum`) — только токены USDT/USDC. * **AVAX / `avalanche` поддерживается** (нативный на C‑Chain). Полная таблица — [Поддерживаемые сети и валюты](/reference/basics-networks). Сверяйтесь программно через [`/v1/payment/services`](/reference/payment-services) и [`/v1/payout/services`](/reference/payout-services). *** ## Суммы Суммы — **строки в единицах валюты** (`"25.00"`), без float. Часть полей — в **minor‑единицах** (например `earnings_by_asset`, `min_minor`); это всегда оговорено на странице метода. → [Форматы сумм и денег](/reference/basics-money) *** ## Чего в публичном API нет * **Ротация ключа** — только в личном кабинете под 2FA, не по API. * **Вывод из личного кошелька** — только в кабинете под 2FA; по API доступен лишь ввод в личный кошелёк. → [Безопасность в проде](/guides/production-security) *** ## Мини‑чек‑лист миграции * Подпись запроса пересобрана под формат Oblodai и совпадает байт‑в‑байт с телом. * Курсы переведены на `POST /v1/exchange-rate/list`, учтено, что котировка к USDT. * Разбор ответов идёт через конверт `state`/`result` и `error.code`; учтено исключение `/v1/webhooks`. * Проверка подписи вебхука переведена на алгоритм Oblodai (timestamp + `.` + сырое тело). * Из кода убраны неподдерживаемые сети (LTC/DOGE/BCH/DASH/XMR; нативный ETH на L2). * Обработка `order_id`‑идемпотентности сохранена для платежей и выплат (для выплат `order_id` обязателен), и **дополнительно** отправляется HTTP‑заголовок `Idempotency-Key` — одно значение на действие, неизменное во всех ретраях. → [Идемпотентность](/reference/basics-idempotency) * `409` **не** трактуется как «финал»: `payout.funds_maturing` и `idempotency.in_progress` надо повторять позже. → [Устойчивый клиент](/guides/resilient-client#не-все-409-финальны) *** ## Связанные страницы # Инструкции Oblodai Source: https://docs.oblodai.com/guides/overview Пошаговые руководства: от получения ключей до боевого приёма платежей. В отличие от [Справочника](/reference/overview), где сухо описан каждый метод, здесь задачи решаются целиком: с контекстом, порядком действий и объяснением, почему именно так. ## Что такое Oblodai Oblodai — платёжный шлюз для приёма и отправки криптовалюты. Интеграция строится на двух вещах: * **HTTP API** — ваш backend вызывает его server‑to‑server, подписывая каждый запрос HMAC‑подписью. Через него вы создаёте платежи, получаете статусы, делаете выплаты и возвраты, управляете вебхуками. * **Hosted‑страница оплаты** — при создании платежа шлюз возвращает ссылку/адрес, на который покупатель отправляет средства. Отслеживание поступления, подтверждения и переплаты/недоплаты берёт на себя шлюз, а вам присылает вебхук. Три принципа, на которых стоит вся интеграция: 1. **Один API‑ключ на весь функционал** — и приём, и выплаты. Радиус поражения ключа ограничен одним вашим мерчантом. За хранение ключа отвечаете вы. 2. **Всё server‑to‑server** — секрет ключа считает подпись только на сервере и никогда не попадает в браузер или мобильное приложение. (Исключение — [платёжные ссылки](/guides/payment-links): они работают без вашего кода вообще, поэтому им сервер не нужен.) 3. **Идемпотентность — два механизма.** HTTP‑заголовок **`Idempotency-Key`** (одно значение на действие, неизменное во всех попытках; повтор вернёт тот же ответ) **и** ваш **`order_id`**, который тоже дедуплицирует (для выплат обязателен). Повторять запрос безопасно. → [Устойчивый клиент](/guides/resilient-client#главный-принцип) ## Начало работы **Начните отсюда:** кабинет, ключи, что понадобится (сервер, вебхуки). От получения ключей до первого вебхука за пять шагов. Разбор HMAC‑подписи по шагам и частые ошибки. Рабочий сервер приёма платежей целиком. Как всё проверить без песочницы. ## Приём платежей Создать счёт, показать адрес, дождаться оплаты. Цена в **фиате** (23 валюты), выбор монеты покупателем, фиксированная валюта и сеть. Оплата **без своего бэкенда**: донаты, диапазоны сумм, кнопка «Оплатить» на Tilda/Wix. Настройки `accuracy` и `autorefund`. Персональный постоянный адрес пополнения для клиента. Автоматический чек плательщику и отправка счёта письмом. Откуда деньги приходят и куда уходят. ## Выплаты и возвраты От баланса до подтверждающего вебхука. Полный и частичный; адрес указывать **не обязательно**. `/v1/payout/mass` (до 100) — когда он ещё уместен; основной путь — [батчи](/guides/batch-operations). ## Масштаб и автоматизация До **5000** платежей, возвратов или выплат **одним** запросом — штатный способ не упираться в rate limit. Доля каждого платежа автоматически уходит партнёрам. Для партнёрок и маркетплейсов. ## Вебхуки Регистрация, проверка подписи, идемпотентность обработчика. Тестовые события, журнал доставок, переотправка. Replay‑защита и перепроверка статуса — за пределами подписи. ## Прод и надёжность Таймауты, идемпотентность, ретраи, backoff. **Какие `409` надо повторять, а какие нет.** Хранение ключа, IP‑allowlist, ротация ключа. Что проверить до боевого трафика. Частые вопросы — короткие ответы. Пошаговая диагностика по симптому. ## Миграция Отличия API и что поправить в коде. # Платёжные ссылки Source: https://docs.oblodai.com/guides/payment-links Платёжная ссылка — это постоянный URL, по которому кто угодно может вам заплатить. Вы создаёте её один раз, дальше она работает сама: покупатель открывает ссылку, при необходимости вводит сумму, выбирает монету и сеть, платит — и на вашем балансе появляются деньги. **Это единственный способ принимать платежи БЕЗ своего бэкенда.** Обычная интеграция требует сервера: каждый вызов API подписывается секретом, а секрет нельзя отдавать в браузер. У платёжной ссылки этой проблемы нет — страница оплаты живёт на стороне Oblodai и работает по **публичным** эндпоинтам, без вашего ключа. Поэтому ссылку можно повесить на кнопку в Tilda/Wix, отправить в мессенджере или положить в шапку профиля. Справочник методов — [`POST /v1/payment/link`](/reference/payment-link) и [публичные методы страницы оплаты](/reference/link-public). *** ## Когда это ваш инструмент * **Сайт на конструкторе** (Tilda, Wix, Webflow) — сервера нет, писать код негде. * **Донаты и чаевые** — сумму называет плательщик, а не вы. * **Продажи в мессенджере/соцсетях** — сайта вообще нет, есть только диалог. * **Разовый счёт клиенту** — выставить и отправить, не заводя интеграцию. Если у вас есть бэкенд и вы хотите автоматически выдавать товар по факту оплаты — вам, скорее всего, нужен обычный счёт: [Приём первого платежа](/guides/accept-first-payment). Ссылка и счёт не исключают друг друга: ссылка при оплате **создаёт настоящий платёж**, с теми же статусами и теми же вебхуками. *** ## Шаг 1. Создать ссылку ```python Python theme={null} link = call("/v1/payment/link", { "title": "Подписка Pro", "description": "Доступ на 30 дней", "amount_mode": "fixed", "currency": "USD", # валюта ЦЕНЫ "amount_fixed": "10.00", "expires_in": 0, # 0 = бессрочная })["result"] print(link["link_id"]) # для управления ссылкой print(link["url"]) # ЭТО и даёте покупателю ``` ```js Node.js theme={null} const link = (await call("/v1/payment/link", { title: "Подписка Pro", description: "Доступ на 30 дней", amount_mode: "fixed", currency: "USD", // валюта ЦЕНЫ amount_fixed: "10.00", expires_in: 0, // 0 = бессрочная })).result; console.log(link.link_id); // для управления ссылкой console.log(link.url); // ЭТО и даёте покупателю ``` Всё. `url` можно вешать на кнопку, отправлять в чат, печатать в QR‑коде. *** ## Шаг 2. Выбрать режим суммы Главное решение при создании — **кто называет сумму**. Режимов три. | `amount_mode` | Кто определяет сумму | Обязательные поля | Типичный сценарий | | ------------- | ------------------------------ | --------------------------- | ---------------------- | | `fixed` | **Вы**, заранее | `amount_fixed` | Товар, подписка | | `open` | **Покупатель** | — (`amount_min` опционален) | Донаты, чаевые | | `range` | **Покупатель**, в ваших рамках | `amount_min` + `amount_max` | Пополнение, предоплата | ### `fixed` — сумма зафиксирована ```python Python theme={null} call("/v1/payment/link", { "title": "Курс по Python", "amount_mode": "fixed", "currency": "USD", "amount_fixed": "49.00", "expires_in": 0, }) ``` ```js Node.js theme={null} await call("/v1/payment/link", { title: "Курс по Python", amount_mode: "fixed", currency: "USD", amount_fixed: "49.00", expires_in: 0, }); ``` Покупатель сумму не вводит и не меняет — он видит 49 USD и платит их. ### `open` — сумму вводит покупатель (донаты) ```python Python theme={null} call("/v1/payment/link", { "title": "Поддержать проект", "amount_mode": "open", "currency": "USD", "amount_min": "1.00", # необязательный ПОЛ: меньше $1 не примем "expires_in": 0, }) ``` ```js Node.js theme={null} await call("/v1/payment/link", { title: "Поддержать проект", amount_mode: "open", currency: "USD", amount_min: "1.00", // необязательный ПОЛ: меньше $1 не примем expires_in: 0, }); ``` Поле ввода суммы на странице оплаты пустое — сколько напишет плательщик, столько и заплатит. Верхней границы нет. `amount_min` — не обязателен, но полезен: он отсекает копеечные платежи, которые целиком съест комиссия сети. ### `range` — сумма в диапазоне ```python Python theme={null} call("/v1/payment/link", { "title": "Пополнение баланса", "amount_mode": "range", "currency": "USD", "amount_min": "5.00", "amount_max": "1000.00", "expires_in": 0, }) ``` ```js Node.js theme={null} await call("/v1/payment/link", { title: "Пополнение баланса", amount_mode: "range", currency: "USD", amount_min: "5.00", amount_max: "1000.00", expires_in: 0, }); ``` Покупатель вводит сумму, но шлюз проверит её по границам: меньше `amount_min` → `paylink.below_min`, больше `amount_max` → `paylink.above_max`. ### Валюта цены: не только доллары `currency` — это **валюта ЦЕНЫ**, и она не обязана быть `USD`. Работает любой из 23 поддерживаемых фиатов (EUR, GBP, RUB, UAH, PLN, CZK, TRY…) или монета (`USDT`, `BTC`). Не путайте с валютой **расчёта** — тем, чем реально платят. ```python Python theme={null} # Донат «от €5», бессрочный call("/v1/payment/link", {"title": "Поддержать проект", "amount_mode": "open", "currency": "EUR", "amount_min": "5", "expires_in": 0}) # Фиксированные 5000 ₽ call("/v1/payment/link", {"title": "Консультация", "amount_mode": "fixed", "currency": "RUB", "amount_fixed": "5000", "expires_in": 0}) ``` ```js Node.js theme={null} // Донат «от €5», бессрочный await call("/v1/payment/link", { title: "Поддержать проект", amount_mode: "open", currency: "EUR", amount_min: "5", expires_in: 0 }); // Фиксированные 5000 ₽ await call("/v1/payment/link", { title: "Консультация", amount_mode: "fixed", currency: "RUB", amount_fixed: "5000", expires_in: 0 }); ``` **Тенге, сом и сум не поддерживаются** — попытка вернёт `400 paylink.unknown_currency`. Полный список и причина — [Три режима создания счёта](/guides/payment-modes#цену-можно-назначать-не-только-в-долларах). **Знаки после запятой у суммы, которую вводит покупатель, проверяются по валюте ЦЕНЫ.** В `open`/`range`‑ссылке с ценой в евро «5.99» пройдёт, а в ссылке с ценой в **иене** «500.50» будет отвергнуто: у `JPY` (и `KRW`) **нет дробной части**. Если вы рисуете свою форму ввода — не давайте вводить копейки там, где валюта их не имеет. *** ## Шаг 3. Решить, кто выбирает монету и сеть Второе решение: **закреплять ли валюту расчёта**. ```python Python theme={null} # Вариант А. Покупатель выбирает сам (pinned_* не заданы) call("/v1/payment/link", { "title": "Донат", "amount_mode": "open", "currency": "USD", "expires_in": 0, }) # → на странице оплаты покупатель сам выбирает: USDT/tron, BTC/bitcoin, TRX/tron… # Вариант Б. Вы закрепили монету и сеть call("/v1/payment/link", { "title": "Подписка Pro", "amount_mode": "fixed", "currency": "USD", "amount_fixed": "10.00", "pinned_currency": "USDT", # валюта расчёта "pinned_network": "tron", # сеть "expires_in": 0, }) # → выбора нет: платят USDT в сети Tron ``` ```js Node.js theme={null} // Вариант А. Покупатель выбирает сам (pinned_* не заданы) await call("/v1/payment/link", { title: "Донат", amount_mode: "open", currency: "USD", expires_in: 0, }); // → на странице оплаты покупатель сам выбирает: USDT/tron, BTC/bitcoin, TRX/tron… // Вариант Б. Вы закрепили монету и сеть await call("/v1/payment/link", { title: "Подписка Pro", amount_mode: "fixed", currency: "USD", amount_fixed: "10.00", pinned_currency: "USDT", // валюта расчёта pinned_network: "tron", // сеть expires_in: 0, }); // → выбора нет: платят USDT в сети Tron ``` Не закрепляйте без нужды — чем шире выбор, тем выше конверсия. Закрепление имеет смысл, если вы сознательно принимаете только один актив (например, только USDT в дешёвой сети, чтобы не разбирать зоопарк монет на балансе). *** ## Шаг 4. Срок жизни ```python Python theme={null} "expires_in": 0 # БЕССРОЧНАЯ — работает, пока вы её не выключите "expires_in": 86400 # живёт сутки с момента создания (секунды) ``` ```js Node.js theme={null} expires_in: 0 // БЕССРОЧНАЯ — работает, пока вы её не выключите expires_in: 86400 // живёт сутки с момента создания (секунды) ``` * **Бессрочная (`0`)** — для донатов, кнопки «Оплатить» на сайте, ссылки в профиле. Такая ссылка принимает платежи многократно, от разных людей. * **С TTL** — для разового счёта конкретному клиенту: выставили, дали срок, дальше ссылка протухает. *** ## Что происходит, когда покупатель платит Страница оплаты работает по **публичным** эндпоинтам — без вашего API‑ключа: ``` 1. Покупатель открывает url ↓ страница читает GET /v1/link/{id} — заголовок, описание, режим суммы 2. Вводит сумму (если режим open/range) и выбирает монету (если не закреплена) ↓ 3. POST /v1/link/{id}/checkout → создаётся НАСТОЯЩИЙ платёж, возвращается его uuid ↓ 4. Дальше всё как у обычного счёта: адрес, QR, подтверждения сети, статусы ↓ 5. Оплачено → на балансе деньги; если у вас есть бэкенд — придёт вебхук invoice.paid ``` Ключевое: **`/v1/link/{id}/checkout` создаёт обычный платёж.** Одна ссылка порождает столько платежей, сколько раз по ней заплатили. Никакой отдельной сущности «оплата ссылки» нет — есть привычные платежи, которые вы видите в истории, по которым приходят вебхуки и с которых делаются возвраты. → [Публичные методы ссылки](/reference/link-public) *** ## Как узнать, что по ссылке заплатили Способ зависит от того, есть ли у вас сервер. **Есть бэкенд** — ловите вебхук `invoice.paid`, как для любого платежа. → [Настройка вебхуков](/guides/webhooks-setup) **Нет бэкенда** — смотрите платежи по ссылке в кабинете или запрашивайте их: ```python Python theme={null} info = call("/v1/payment/link/info", { "link_id": "lnk_…", "limit": 50, "offset": 0, })["result"] # в ответе — сама ссылка и список платежей, созданных по ней ``` ```js Node.js theme={null} const info = (await call("/v1/payment/link/info", { link_id: "lnk_…", limit: 50, offset: 0, })).result; // в ответе — сама ссылка и список платежей, созданных по ней ``` Хотите **чек покупателю на почту** — на странице оплаты есть поле email, и `checkout` принимает `payer_email`. Чек уйдёт автоматически после оплаты. → [Счёт и чек на почту](/guides/email-invoices) *** ## Управление ссылками ```python Python theme={null} # Список всех ссылок call("/v1/payment/link/list", {"limit": 50, "offset": 0}) # Выключить ссылку (перестанет принимать платежи) call("/v1/payment/link/toggle", {"link_id": "lnk_…", "active": False}) # Включить обратно call("/v1/payment/link/toggle", {"link_id": "lnk_…", "active": True}) ``` ```js Node.js theme={null} // Список всех ссылок await call("/v1/payment/link/list", { limit: 50, offset: 0 }); // Выключить ссылку (перестанет принимать платежи) await call("/v1/payment/link/toggle", { link_id: "lnk_…", active: false }); // Включить обратно await call("/v1/payment/link/toggle", { link_id: "lnk_…", active: true }); ``` Выключение **обратимо** и не трогает уже созданные по ссылке платежи — они живут своей жизнью. Это основной способ «закрыть» бессрочную ссылку: удалять её не нужно. *** ## Ошибки Ошибки создания (ваш вызов): | Код | Значение | | ------------------------------------- | ------------------------------------------------------------------------- | | `paylink.bad_mode` | `amount_mode` не `fixed`/`open`/`range`. | | `paylink.bad_amount` | Некорректная сумма. | | `paylink.not_positive` | Сумма ≤ 0. | | `paylink.bad_min` / `paylink.bad_max` | Некорректная нижняя/верхняя граница. | | `paylink.bad_range` | `amount_min` больше `amount_max`. | | `paylink.unknown_currency` | Валюта не поддерживается. → [`GET /v1/currencies`](/reference/currencies) | | `paylink.disabled` | Ссылки временно выключены (`503`) — повторите позже. | Ошибки оплаты (их увидит покупатель на странице): | Код | Значение | | -------------------------------------- | ----------------------------------------------------------- | | `paylink.not_found` / `paylink.bad_id` | Ссылки нет, она выключена или истёк её срок (`expires_in`). | | `paylink.amount_required` | Режим `open`/`range`, а сумма не введена. | | `paylink.below_min` | Сумма меньше `amount_min`. | | `paylink.above_max` | Сумма больше `amount_max`. | *** ## Связанные страницы публичные методы. что делать, если сервера нет. автоматический чек плательщику. цена vs валюта расчёта. обычный счёт, когда бэкенд есть. # Три режима создания счёта Source: https://docs.oblodai.com/guides/payment-modes Метод [`POST /v1/payment`](/reference/payment-create) умеет создавать счёт тремя способами — в зависимости от того, насколько заранее вы (или покупатель) определяете валюту и сеть оплаты. **Главный сценарий: цена в фиате, монету выбирает покупатель.** У вас в магазине ценник в привычной валюте (`USD`, `EUR`, `RUB`…), а чем платить — USDT, BTC, TRX — и в какой сети решает сам покупатель на странице оплаты. Это **Режим 3**, и именно с него стоит начинать: он ничего не требует, кроме суммы и валюты цены, и даёт покупателю максимальный выбор (а значит, конверсию). Режимы 1 и 2 нужны, когда вы **сознательно** сужаете выбор — например, принимаете только USDT в дешёвой сети, чтобы не разгребать зоопарк монет на балансе. *** ## Сначала: цена и расчёт — это разные валюты Режимы касаются **валюты расчёта**, а не валюты цены. Не путайте два поля: | Поле | Роль | Что может быть | | ------------- | ---------------------------------- | ----------------------------------------------------------------------------------------- | | `currency` | **Цена**: сколько счёт стоит. | Любой из **23 фиатов** (`USD`, `EUR`, `RUB`, …) **или** крипта (`USDT`, `BTC`, `TRX`, …). | | `to_currency` | **Расчёт**: чем покупатель платит. | **Только крипта.** Фиат здесь невозможен. | Шлюз сам пересчитает фиатную цену в крипту по курсу (округляя **вверх**, в пользу счёта): ```python Python theme={null} call("/v1/payment", {"amount": "10", "currency": "USD", "order_id": "order-1", "to_currency": "TRX", "network": "tron"}) # → amount: "10.00" USD, payer_amount: "83.333334" TRX ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "10", currency: "USD", order_id: "order-1", to_currency: "TRX", network: "tron" }); // → amount: "10.00" USD, payer_amount: "83.333334" TRX ``` Баланс, выплаты и возвраты при этом **всегда в крипте** — фиат шлюз не хранит. См. [Форматы сумм и денег](/reference/basics-money). *** ## Цену можно назначать не только в долларах Валюта цены — это **не обязательно `USD`**. Поддерживаются **23 фиатные валюты**: | | | | -------------------------- | ----------------------------------------------------- | | **Европа** | `EUR` · `GBP` · `PLN` · `CZK` · `CHF` | | **СНГ / Восточная Европа** | `RUB` · `UAH` | | **Америка** | `USD` · `CAD` · `BRL` · `MXN` | | **Азия** | `CNY` · `INR` · `JPY` · `KRW` · `IDR` · `THB` · `VND` | | **Прочие** | `TRY` · `AED` · `ZAR` · `AUD` · `NGN` | Работают ровно так же, как `USD`, — во всех трёх режимах: ```python Python theme={null} call("/v1/payment", {"amount": "100", "currency": "EUR", "order_id": "order-8", "to_currency": "USDT", "network": "tron"}) # → 100.00 EUR ≈ 108.695653 USDT call("/v1/payment", {"amount": "5000", "currency": "RUB", "order_id": "order-2", "to_currency": "USDT", "network": "tron"}) # → 5000.00 RUB ≈ 54.347827 USDT call("/v1/payment", {"amount": "2000", "currency": "UAH", "order_id": "order-3"}) # → валюто-агностичный счёт (is_multi: true), цена в гривне, монету выберет покупатель ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "100", currency: "EUR", order_id: "order-8", to_currency: "USDT", network: "tron" }); // → 100.00 EUR ≈ 108.695653 USDT await call("/v1/payment", { amount: "5000", currency: "RUB", order_id: "order-2", to_currency: "USDT", network: "tron" }); // → 5000.00 RUB ≈ 54.347827 USDT await call("/v1/payment", { amount: "2000", currency: "UAH", order_id: "order-3" }); // → валюто-агностичный счёт (is_multi: true), цена в гривне, монету выберет покупатель ``` **Цена в евро так же надёжна, как в долларах.** Источник курсов отдаёт цену монеты **сразу в нужной валюте**, одной таблицей — ничего не перемножается через доллар и второго провайдера в цепочке нет. Так что выбирать `USD` «для надёжности» смысла не имеет: берите валюту, в которой у вас реальный ценник. ### У JPY и KRW — ноль знаков после запятой Иена и вона не имеют дробной части. Сумма передаётся **целым числом**: ```python Python theme={null} call("/v1/payment", {"amount": "10000", "currency": "JPY", "order_id": "order-4", "to_currency": "USDT", "network": "tron"}) # → 10000 JPY ≈ 66.666667 USDT ✅ "10000", а НЕ "10000.00" ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "10000", currency: "JPY", order_id: "order-4", to_currency: "USDT", network: "tron" }); // → 10000 JPY ≈ 66.666667 USDT ✅ "10000", а НЕ "10000.00" ``` У остальных 21 фиатной валюты — 2 знака (`"10.00"`). ### Что НЕ поддерживается **Тенге (`KZT`), сом (`KGS`) и сум (`UZS`) — не поддерживаются.** Попытка вернёт `400 payment.unknown_currency`: ```python Python theme={null} call("/v1/payment", {"amount": "5000", "currency": "KZT"}) # … остальные поля # → 400 payment.unknown_currency ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "5000", currency: "KZT", /* … */ }); // → 400 payment.unknown_currency ``` Причина: источник курсов не котирует крипту в этих валютах напрямую, а считать через доллар (два курса подряд) мы не будем — это вторая точка отказа и второй источник расхождений в цене. Обходной путь: назначайте цену в `USD` и пересчитывайте в тенге/сом/сум **у себя на витрине**. Актуальный список валют цены всегда можно получить программно — в [`GET /v1/currencies`](/reference/currencies) это блок **`pricing_currencies`** (монеты + фиаты, у фиатов стоит `"fiat": true` и указан `decimals`). Не хардкодьте список — читайте его. *** ## Режим 1. Фиксированная валюта и сеть Вы точно знаете, чем и в какой сети платит покупатель. Задаёте `to_currency` **и** `network`. ```python Python theme={null} call("/v1/payment", { "amount": "10", "currency": "USD", "order_id": "order-5", "to_currency": "USDT", "network": "tron", }) ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "10", currency: "USD", order_id: "order-5", to_currency: "USDT", network: "tron", }); ``` Результат: обычный одновалютный счёт с готовым адресом сразу. **Когда использовать:** у вас на чекауте покупатель уже выбрал «USDT в сети Tron», и вы передаёте конкретную пару. *** ## Режим 2. Авто‑нормализация сети Вы задаёте валюту (`to_currency`/`currency`), но **не** задаёте `network`. ```python Python theme={null} call("/v1/payment", { "amount": "10", "currency": "USD", "order_id": "order-6", "to_currency": "BTC", # network не указан }) ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "10", currency: "USD", order_id: "order-6", to_currency: "BTC", // network не указан }); ``` * Если у валюты **ровно одна** сеть (как у `BTC` → `bitcoin`), она подставится автоматически. * Если у валюты **несколько** сетей (как у `USDT`), вернётся ошибка `payment.network_required` — сеть нужно указать явно. **Когда использовать:** для «односетевых» валют, чтобы не писать сеть руками. *** ## Режим 3. Валюто‑агностичный (deferred) счёт Это и есть **главный сценарий** из начала страницы: ценник в фиате, монету и сеть выбирает покупатель. Валюто‑агностичный (deferred) счёт — это счёт без заранее выбранной валюты и сети: их выбирает не вы, а сам покупатель, уже на странице оплаты. Вы **не** задаёте ни `to_currency`, ни `network` — оба пустые. Валюту и сеть выбирает **покупатель** на hosted‑странице (страница оплаты на стороне Oblodai, куда вы редиректите покупателя по ссылке `url`). ```python Python theme={null} inv = call("/v1/payment", { "amount": "10", "currency": "USD", "order_id": "order-7", # to_currency и network НЕ переданы })["result"] assert inv["is_multi"] is True # признак агностичного счёта assert inv["payment_status"] == "select" # статус «монета ещё не выбрана» # payer_currency — ПУСТАЯ строка; address, payer_amount, address_qr_code тоже пусты, # пока покупатель не выбрал монету (валюты расчёта у счёта ещё нет). # А amount/currency — уже есть: "10" USD. ``` ```js Node.js theme={null} const inv = (await call("/v1/payment", { amount: "10", currency: "USD", order_id: "order-7", // to_currency и network НЕ переданы })).result; console.assert(inv.is_multi === true); // признак агностичного счёта console.assert(inv.payment_status === "select"); // статус «монета ещё не выбрана» // payer_currency — ПУСТАЯ строка; address, payer_amount, address_qr_code тоже пусты, // пока покупатель не выбрал монету (валюты расчёта у счёта ещё нет). // А amount/currency — уже есть: "10" USD. ``` **Пустой `address` и пустой `payer_currency` — это не баг.** Пока счёт в статусе `select`, валюты расчёта у него просто нет, а значит, неоткуда взяться ни адресу, ни сумме в крипте. По той же причине [возврат](/guides/refunds) такого счёта вернёт `refund.nothing_to_refund` — возвращать нечего. Поле `is_multi` в ответе — это флаг «мультивалютности»: `true` означает, что счёт агностичный и валюту ещё предстоит выбрать покупателю, `false` — валюта и сеть уже зафиксированы (Режимы 1 и 2). Дальше на странице оплаты покупатель вызывает [`POST /v1/pay/{id}/select`](/reference/pay-select) с выбранной парой — тогда фиксируется курс и выделяется адрес. **Когда использовать:** вы хотите дать покупателю выбор монеты прямо на странице оплаты, не спрашивая заранее. ### Чем ограничить выбор По умолчанию покупателю доступен весь каталог. Чтобы сузить список принимаемых валют, задайте набор через [`POST /v1/payment/accepted/set`](/reference/payment-accepted): ```python Python theme={null} call("/v1/payment/accepted/set", {"accepted": [ {"currency": "USDT", "network": "tron"}, {"currency": "USDC", "network": "polygon"}, ]}) ``` ```js Node.js theme={null} await call("/v1/payment/accepted/set", { accepted: [ { currency: "USDT", network: "tron" }, { currency: "USDC", network: "polygon" }, ]}); ``` Выбор покупателя валидируется против этого набора (или полного каталога, если набор пуст). *** ## Сравнение | | Режим 1 | Режим 2 | **Режим 3** | | --------------------------- | --------------------- | ------------------ | ------------------------ | | `to_currency` | задан | задан | пусто | | `network` | задан | пусто | пусто | | `currency` (цена) | фиат или крипта | фиат или крипта | фиат или крипта | | Адрес в ответе | сразу | сразу | после выбора покупателем | | `is_multi` | `false` | `false` | `true` | | Статус сразу после создания | `check` | `check` | **`select`** | | Кто выбирает валюту | вы | вы (сеть авто) | **покупатель** | | Когда брать | Принимаете один актив | Односетевая монета | **По умолчанию** | *** ## Частая ошибка: цена в фиате и сеть без монеты Если цена задана **в фиате** (`USD`, `EUR`, `RUB` — любом), и вы задали `network`, но **не** задали `to_currency`, шлюз вернёт `400 payment.to_currency_required`: ```python Python theme={null} call("/v1/payment", {"amount": "10", "currency": "USD", "network": "tron"}) # → 400 payment.to_currency_required call("/v1/payment", {"amount": "100", "currency": "EUR", "network": "tron"}) # → 400 payment.to_currency_required — правило то же для ЛЮБОГО фиата ``` ```js Node.js theme={null} await call("/v1/payment", { amount: "10", currency: "USD", network: "tron" }); // → 400 payment.to_currency_required await call("/v1/payment", { amount: "100", currency: "EUR", network: "tron" }); // → 400 payment.to_currency_required — правило то же для ЛЮБОГО фиата ``` Из фиатной цены монету расчёта вывести нельзя: в сети `tron` это может быть и USDT, и TRX, и USDC. Два выхода: * **укажите монету** — `"to_currency": "USDT"` (Режим 1); * **уберите и `network`** — тогда монету и сеть выберет покупатель (Режим 3). Умолчание «`to_currency` = `currency`» срабатывает, только когда цена задана **в крипте**: для `{"currency": "USDT", "network": "tron"}` расчёт пойдёт в `USDT`. С фиатом такого умолчания нет и быть не может — фиатом в блокчейне не платят. *** ## Полезные детали * **Комиссия** фиксируется уже при создании во всех трёх режимах. В агностичном режиме курс и адрес фиксируются в момент выбора, а комиссия — сразу. * **`is_refresh`** оживляет просроченный счёт по `order_id` вместо создания нового — работает во всех режимах. См. [`POST /v1/payment`](/reference/payment-create). *** ## Связанные страницы те же режимы суммы и валюты, но без бэкенда. до 5000 счетов одним запросом. # Крипто-чеки: выплата без адреса получателя Source: https://docs.oblodai.com/guides/payout-links Отправьте средства ссылкой — получатель сам укажет, куда их зачислить. Классическая выплата требует знать адрес получателя заранее. **Крипто-чек** (выплатная ссылка) решает обратную задачу: вы резервируете сумму и отдаёте получателю **ссылку** — он открывает её, вводит свой адрес, и выплата уходит ему. Идеально для бонусов, призов, кэшбэка, реферальных вознаграждений и любых ситуаций, где спрашивать адрес заранее неудобно. ## Как это работает ```mermaid theme={null} flowchart LR A["Вы создаёте чек
POST /v1/payout/link"] --> B["Резерв: available → payout_held"] B --> C["Получатель открывает claim_url
(можно письмом)"] C --> D["Вводит свой адрес
POST /v1/claim/{token}"] D --> E["Рождается обычная выплата
payout.* вебхуки"] B -->|"срок вышел / отмена"| F["Резерв вернулся на available"] ``` Деньги списываются с доступного баланса сразу при создании (в удержание `payout_held`), поэтому чек **всегда обеспечен**: получатель не может увидеть «недостаточно средств». Невостребованный чек возвращает резерв автоматически по сроку — или раньше, если вы его отмените. ## Шаг 1. Создайте чек ```python Python theme={null} link = call("/v1/payout/link", { "currency": "USDT", "network": "tron", "amount": "25", "reference": "bonus-42", # ваш ключ дедупликации — задавайте всегда "title": "Бонус за июль", "note": "Спасибо за участие в программе!", "email": "user@example.com", # необязательно: мы сами отправим письмо "expires_in_hours": 168, # 7 дней; БЕЗ поля чек живёт всего 1 час })["result"] print(link["claim_url"]) # передайте получателю — показывается ОДИН раз ``` ```js Node.js theme={null} const link = (await call("/v1/payout/link", { currency: "USDT", network: "tron", amount: "25", reference: "bonus-42", // ваш ключ дедупликации — задавайте всегда title: "Бонус за июль", note: "Спасибо за участие в программе!", email: "user@example.com", // необязательно: мы сами отправим письмо expires_in_hours: 168, // 7 дней; БЕЗ поля чек живёт всего 1 час })).result; console.log(link.claim_url); // передайте получателю — показывается ОДИН раз ``` Два правила, о которые спотыкаются: **(1)** `claim_url` показывается **только в ответе создания** — сохраните его сразу, восстановить нельзя; **(2)** всегда задавайте `expires_in_hours` — без него чек истечёт **через час**. Если указали `email`, получателю уйдёт брендированное письмо с кнопкой «Получить средства» и вашим текстом из `note`. Отправка не критична для чека: даже если письмо не дошло, ссылка работает. ## Шаг 2. Получатель забирает средства По ссылке открывается страница получения: сумма, ваш заголовок и сообщение, поле для адреса. Получатель вводит адрес в сети чека — и из резерва рождается **обычная выплата**. Дальше всё как всегда: подтверждения сети, [`payout.*` вебхуки](/reference/webhook-object) вам, средства — получателю. * В событиях выплаты `order_id` = `payoutlink:` — так вы сопоставите вебхук с чеком. * Получение **идемпотентно по адресу**: повторное открытие/отправка того же адреса не создаёт вторую выплату, а другой адрес после первого получения не принимается. * Адрес проходит те же проверки, что у выплат: валидация формата, запрет внутренних адресов шлюза, комплаенс-скрининг. ## Массовая раздача До **500 чеков одним запросом** — `POST /v1/payout/link/batch`. Каждый элемент независим (плохой фейлит только себя), у всех чеков вызова общий `batch_id`: ```python Python theme={null} resp = call("/v1/payout/link/batch", {"links": [ {"currency": "USDT", "network": "tron", "amount": "10", "reference": f"airdrop-{i}", "email": user["email"], "title": "Airdrop", "expires_in_hours": 336} for i, user in enumerate(users) # до 500 за вызов ]})["result"] for r in resp["results"]: if r["ok"]: save_claim_url(r["link"]["reference"], r["link"]["claim_url"]) else: log_failed(r["error"], r["message"]) ``` ```js Node.js theme={null} const resp = (await call("/v1/payout/link/batch", { links: users.slice(0, 500).map((u, i) => ({ currency: "USDT", network: "tron", amount: "10", reference: `airdrop-${i}`, email: u.email, title: "Airdrop", expires_in_hours: 336, })), })).result; for (const r of resp.results) { if (r.ok) saveClaimUrl(r.link.reference, r.link.claim_url); else logFailed(r.error, r.message); } ``` Больше 500 получателей — шлите страницами. Нужна раздача на **известные** адреса? Это обычные [массовые выплаты](/guides/batch-operations), чеки не нужны. ## Жизненный цикл и учёт | Событие | Баланс | Статус чека | | -------------------------------------- | ---------------------------- | ----------- | | Создание | `available → payout_held` | `funded` | | Получатель забрал | `payout_held` → выплата ушла | `claimed` | | Срок вышел (поллер, раз в минуту) | `payout_held → available` | `expired` | | Вы отменили (`/v1/payout/link/cancel`) | `payout_held → available` | `cancelled` | Мониторьте выданные чеки через `/v1/payout/link/list` (новые первыми) и `/info` по конкретному. Отменить можно только невостребованный (`funded`) чек; если получение уже началось — вернётся статус `claimed` с `payout_id`, двойного возврата не бывает. **Дедупликация — через `reference`.** Заголовок `Idempotency-Key` на `/v1/payout/link` не действует. Повтор создания с тем же `reference` сейчас возвращает `500` (а не повтор ответа) — не ретрайте вслепую, проверьте `/list`, появился ли чек. ## Связанные страницы Что такое `payout_held` и дозревание средств. # Чек‑лист перед запуском в прод Source: https://docs.oblodai.com/guides/production-checklist Пройдите этот список перед первым боевым трафиком. Каждый пункт ссылается на подробную страницу. Если весь список пугает объёмом — не пугайтесь. Сначала закройте короткий «минимум» ниже: этого достаточно, чтобы принять первый платёж безопасно. Остальное можно докручивать по мере роста. *** ## Минимум, чтобы принять первый платёж безопасно Пять пунктов, без которых нельзя выходить на боевой трафик: * **Секрет не лежит в коде.** Ключ читается из переменных окружения или секрет‑хранилища, не из репозитория и не с фронтенда. → [Безопасность в проде](/guides/production-security#1-хранение-секрета) * **Есть HTTPS‑endpoint для вебхуков**, доступный из интернета, и он **проверяет подпись** по сырому телу. → [Настройка вебхуков](/guides/webhooks-setup) * **Статус заказа меняется только по вебхуку** — и перепроверяется запросом [`/v1/payment/info`](/reference/payment-info), а не по редиректу пользователя. → [Приём первого платежа](/guides/accept-first-payment) * **Обработка вебхука идемпотентна** — дедуп по `uuid` + `status`, повтор той же доставки ничего не ломает. → [Настройка вебхуков](/guides/webhooks-setup#обрабатывать-идемпотентно) * **Суммы хранятся строками** (или в minor‑единицах), без float. → [Форматы сумм и денег](/reference/basics-money) Закрыли эти пять — можно принимать реальные деньги. Дальше идёт полный харденинг. *** ## Полный прод‑харденинг (по мере роста) Когда базовый чек‑лист пройден и трафик растёт, пройдитесь по областям ниже. ### Безопасность и ключи * **Секрет ключа хранится только на сервере** — в секрет‑хранилище, не в браузере/мобильном приложении, не в репозитории. → [Безопасность в проде](/guides/production-security) * **Включён IP‑allowlist** для API‑ключа: сначала добавлен IP backend, затем включён контроль. → [IP‑allowlist](/reference/api-allowlist) * **Понятно, как ротировать ключ** — только в личном кабинете под 2FA; в публичном API ротации нет; заморозки выплат при ротации нет. → [Безопасность в проде](/guides/production-security#3-ротация-ключа) * **Часы сервера синхронизированы** (NTP) — при расхождении больше ±5 минут подпись не проходит и приходит `merchant.bad_signature` (окно времени проверяется внутри сверки подписи). → [Как подписать запрос](/guides/signing-requests) *** ### Подпись и идемпотентность * **Тело сериализуется один раз** и используется и для подписи, и для отправки (байт‑в‑байт). → [Как подписать запрос](/guides/signing-requests) * **Заголовок `Idempotency-Key` на каждом денежном запросе.** Значение генерируется **до первой отправки** и одинаково во **всех** попытках одного действия. Повтор вернёт тот же ответ и заголовок `Idempotent-Replayed: true`. → [Идемпотентность](/reference/basics-idempotency) * **Каждый платёж создаётся с уникальным `order_id`**, каждая выплата — с уникальным `order_id`/`reference` (для выплат он **обязателен**). Это второй, бизнес‑уровень дедупликации: он работает, даже если заголовок забыли. → [Идемпотентность](/reference/basics-idempotency) * **Ретраи с backoff на `429`/`503`/`500`.** * **`409` — НЕ всегда финал.** Нельзя трактовать любой `409` как «уже обработано» и бросать операцию: так вы **потеряете законную выплату**. Разделяйте: * `409 payout.funds_maturing` — **повторяемая**: средства ещё дозревают, повторите позже с backoff, и выплата пройдёт. * `409 idempotency.in_progress` — **повторяемая**: запрос с этим `Idempotency-Key` ещё выполняется, подождите и повторите. * прочие `409` (например `payout.insufficient_funds`) — конфликт состояния: **не** повторяйте вслепую, сверьтесь через `*/info`. → [Рецепт устойчивого клиента](/guides/resilient-client#какие-ошибки-повторять-а-какие-—-нет) *** ### Вебхуки * **Приёмник вебхуков настроен и проверен**: endpoint зарегистрирован, секрет сохранён. → [Настройка вебхуков](/guides/webhooks-setup) * **Подпись вебхука проверяется правильным алгоритмом** (timestamp + `.` + сырое тело), по сырому телу. → [Объект вебхука](/reference/webhook-object) * **Обработка идемпотентна**: дедуп по `uuid` + `status`, порядок доставок не подразумевается. → [Настройка вебхуков](/guides/webhooks-setup#обрабатывать-идемпотентно) * **`2xx` возвращается только после успешной обработки.** * **Пробное событие доходит** и возвращает `status_code: 200`. → [Отладка вебхуков](/guides/webhooks-debug) *** ### Платежи * **Обрабатываются все статусы** платежа, включая `wrong_amount`, `paid_over`, `cancel`. → [Приём первого платежа](/guides/accept-first-payment) * **Недоплата/переплата настроены осознанно** — допуск `accuracy` и автовозврат `autorefund`. → [Недоплата, переплата и автовозврат](/guides/under-overpayment) * **Для Bitcoin/UTXO учтён ручной возврат** (автовозврат там не работает). → [Автовозврат](/reference/payment-autorefund) *** ### Выплаты и баланс * **Учтены незрелые (maturing) средства** — выводимый остаток может быть меньше показанного; `409 payout.funds_maturing` обрабатывается **как временная ошибка и повторяется**, а не считается отказом. → [`POST /v1/balance`](/reference/balance) * **Адрес получателя валидируется** до вызова выплаты (выплаты необратимы и уходят сразу). → [Первая выплата](/guides/first-payout) * **Большие списки выплат идут через батч** [`POST /v1/payout/batch`](/reference/payout-batch) (до 5000, асинхронно), а не циклом и не пачками по 100. → [Массовые операции](/guides/batch-operations) * **Результат батча разбирается поэлементно** (частичный успех — норма), статус опрашивается через `/v1/batch/info`. → [Массовые операции](/guides/batch-operations) *** ### Эксплуатация * **Мониторинг баланса и статусов выплат** настроен. * **Хранится сопоставление `order_id` ↔ ваш заказ** на вашей стороне. * **Суммы хранятся строками/в minor‑единицах**, без float. → [Форматы сумм и денег](/reference/basics-money) * **Поддерживаемые сети/валюты сверяются программно** через `*/services`, а не только по таблице. → [`POST /v1/payment/services`](/reference/payment-services) *** ## Связанные страницы # Безопасность в проде Source: https://docs.oblodai.com/guides/production-security Oblodai построен на модели «один API‑ключ на весь функционал». Это удобно, но повышает цену утечки: тем же ключом, которым вы принимаете платежи, можно и выводить средства. Эта инструкция — про то, как снизить риски перед боевым запуском. *** ## Модель ключа * **Один ключ — и приём, и выплаты.** Отдельных ключей нет. * **Радиус поражения ограничен одним вашим мерчантом.** Утёкший ключ не трогает чужие данные и баланс, но со **своим** балансом может сделать всё, включая вывод. * **За хранение ключа отвечаете вы.** Отсюда три опоры защиты: хранить секрет правильно, ограничить, откуда им можно пользоваться, и знать, как его сменить. *** ## 1. Хранение секрета * Секрет живёт **только на сервере**, в секрет‑хранилище (Vault, KMS, зашифрованные переменные окружения) — не в коде, не в репозитории, не в браузере, не в мобильном приложении. **Что за Vault/KMS?** Это специальные хранилища секретов (например HashiCorp Vault или облачный Key Management Service), которые держат ключи отдельно от кода и выдают их приложению по запросу. **На старте достаточно переменных окружения** сервера — Vault/KMS подключают по мере роста, когда секретов и людей становится больше. * Подпись считается на бэкенде. Клиент никогда не видит `secret`. * Секрет вебхука (`secret` из [`/v1/webhooks`](/reference/webhooks-register)) — тоже секрет: храните так же. *** ## 2. IP‑allowlist Ограничьте, с каких IP принимаются подписанные запросы. Когда список включён, запрос с чужого IP получает `401 auth.ip_not_allowed`, даже если подпись верна. ```python Python theme={null} # 1) Сначала добавьте IP вашего backend call("/v1/api-allowlist/add", {"cidr": "203.0.113.10"}) # 2) Только потом включайте контроль call("/v1/api-allowlist/enable", {"enabled": True}) ``` ```js Node.js theme={null} // 1) Сначала добавьте IP вашего backend await call("/v1/api-allowlist/add", { cidr: "203.0.113.10" }); // 2) Только потом включайте контроль await call("/v1/api-allowlist/enable", { enabled: true }); ``` **Порядок важен.** Нельзя включить контроль с пустым списком (`apiallow.empty`) — иначе рискуете заблокировать сами себя. Сначала добавьте IP, потом включайте. Управление списком — [`POST /v1/api-allowlist/*`](/reference/api-allowlist). *** ## 3. Ротация ключа **Ротация ключа не входит в публичный API.** По подписанному запросу ключ ротировать нельзя — это сделано специально, чтобы утёкший ключ не мог сам себя сменить и заблокировать владельца. Смена ключа выполняется **только в личном кабинете на сайте**: 1. Владелец мерчанта заходит в кабинет. 2. Проходит подтверждение (2FA — двухфакторная аутентификация: помимо пароля вводится одноразовый код, например из приложения‑аутентификатора). 3. Получает новый `public_id` + `secret` (секрет показывается один раз). Старый секрет перестаёт работать сразу. **Заморозки выплат при ротации нет** — приём и выплаты продолжают работать без паузы. *** ## Особенности денежных операций * **Выплаты по API‑ключу авто‑одобряются и уходят сразу** — без белых списков и периодов выдержки, и они необратимы. Валидируйте адрес получателя на своей стороне до вызова [`/v1/payout`](/reference/payout-create). * **Вывод из личного кошелька — только в кабинете под 2FA.** На API‑ключе доступен лишь ввод средств В личный кошелёк ([`/v1/transfer/to-personal`](/reference/transfer-to-personal)); вывод оттуда через API намеренно закрыт. * **Идемпотентность обязательна.** Отправляйте денежные запросы с HTTP‑заголовком **`Idempotency-Key`** (одно значение на действие, неизменное во всех попытках) и задавайте **`order_id`/`reference`** (для выплат он обязателен). Это два независимых механизма защиты от дублей при ретраях: заголовок — на уровне транспорта, `order_id` — на уровне вашей бизнес‑логики. См. [Идемпотентность](/reference/basics-idempotency). *** ## Дополнительно * **Rate limit.** При превышении — `429` с телом `{"state":1,"message":"rate limit exceeded"}` и заголовком `Retry-After`; подождите и повторяйте с backoff. → [Ограничение частоты](/reference/basics-ratelimit) * **Часы сервера.** Держите время синхронизированным (NTP): подпись живёт в окне ±5 минут, при расхождении сверка подписи не пройдёт и придёт `merchant.bad_signature` (окно проверяется внутри проверки подписи). Код `auth.bad_timestamp` — это только про отсутствующий или нечисловой `X-Timestamp`. * **Мониторинг.** Следите за балансом и статусами выплат; храните сопоставление `order_id` ↔ ваш заказ. *** ## Связанные страницы # Возвраты платежей Source: https://docs.oblodai.com/guides/refunds Возврат отправляет средства оплаченного платежа обратно покупателю. Технически это операция на движке выплат — списание с вашего баланса. Возврат бывает **полный** и **частичный**. Справочник метода — [`POST /v1/payment/refund`](/reference/payment-refund). *** ## Главное: адрес указывать НЕ обязательно Самое частое заблуждение — что для возврата нужно где‑то раздобыть адрес покупателя и передать его в запросе. **Не нужно.** Шлюз запоминает `payer_address` — адрес, с которого реально пришли деньги, — и по умолчанию возвращает средства **именно туда**. Поэтому минимальный полный возврат выглядит так: ```python Python theme={null} call("/v1/payment/refund", {"order_id": "order-1"}) # и всё ``` ```js Node.js theme={null} await call("/v1/payment/refund", { order_id: "order-1" }); // и всё ``` Ни `address`, ни `amount` не нужны: вернётся вся полученная сумма на адрес плательщика. **Не спрашивайте адрес у покупателя без необходимости.** Это лишний шаг, источник опечаток и классический вектор мошенничества («напишите мне другой адрес для возврата»). Возврат на `payer_address` — и проще, и безопаснее. ### Когда `address` всё-таки нужен | Ситуация | `address` | | ----------------------------------------------------------------------- | ---------------------------------------------- | | Обычный возврат покупателю (Tron, Ethereum, BSC, Polygon, Solana, TON…) | **Не нужен** — уйдёт на `payer_address`. | | **Bitcoin и другие UTXO‑сети** | **Обязателен.** Иначе `400 refund.no_address`. | | Возврат на **другой** адрес (не тот, с которого платили) | Нужен — и потребует подтверждения (см. ниже). | **Почему UTXO — исключение.** В сетях вроде Bitcoin у транзакции нет одного однозначного отправителя: она собирается из нескольких входов, которые могут принадлежать разным адресам (в том числе адресам биржи или чужого кошелька). «Адреса плательщика» там попросту не существует как надёжного понятия — поэтому шлюз не угадывает, а требует указать адрес явно. *** ## Полный возврат Не указывайте `amount` — вернётся вся полученная сумма: ```python Python theme={null} call("/v1/payment/refund", { "order_id": "order-1", # или uuid платежа }) ``` ```js Node.js theme={null} await call("/v1/payment/refund", { order_id: "order-1", // или uuid платежа }); ``` Для Bitcoin/UTXO — с адресом: ```python Python theme={null} call("/v1/payment/refund", { "order_id": "order-btc-1", "address": "bc1q...", # для UTXO-сетей обязателен }) ``` ```js Node.js theme={null} await call("/v1/payment/refund", { order_id: "order-btc-1", address: "bc1q...", // для UTXO-сетей обязателен }); ``` *** ## Частичный возврат Укажите `amount` — вернётся только эта часть: ```python Python theme={null} call("/v1/payment/refund", { "order_id": "order-1", "amount": "10", # частичная сумма }) ``` ```js Node.js theme={null} await call("/v1/payment/refund", { order_id: "order-1", amount: "10", // частичная сумма }); ``` Суммарно по платежу **нельзя вернуть больше оплаченного**. Несколько частичных возвратов складываются. *** ## В какой валюте вернутся деньги Возврат деноминирован в **той монете, которая фактически пришла**. Если покупатель заплатил 100 USDT — вернутся USDT, а не «эквивалент по текущему курсу». **Курс заново не пересчитывается.** Это важно понимать: если цена была в фиате (`"currency": "USD"`), а за время до возврата курс монеты изменился, то в долларах покупатель получит не ровно то, что заплатил. Шлюз возвращает монету, а не фиатную стоимость — фиат он вообще не хранит. → [Модель баланса](/guides/balance-and-funds) *** ## Кто платит и что подтверждается * **Адрес плательщика vs произвольный.** По умолчанию возврат уходит на **записанный адрес плательщика** (`payer_address`). Возврат, инициированный по API‑ключу, **авто‑одобряется** и уходит сразу **на любой адрес** — ключ несёт ту же полноту полномочий, что и при обычной выплате; отдельного подтверждения ([`POST /v1/payout/approve`](/reference/payout-approve)) не требуется. Поэтому перепроверяйте `address`, если передаёте его явно: остановить отправленный возврат нельзя. * **Наша комиссия при возврате.** Кто её несёт — клиент (получает net) или мерчант (клиент получает gross) — задаётся настройкой [`refund-fee-config`](/reference/payout-refund-fee-config). Шлюз свою комиссию при возврате не покрывает. **Возврат списывает с вашего баланса ВСЮ сумму, которую заплатил покупатель.** Не «вашу часть после комиссий» — всю. Это критично, если у вас настроены [сплит‑платежи](/guides/split-payments): именно поэтому расчёт по сплитам откладывается на окно удержания. *** ## Отслеживание возврата Есть два способа, и первый удобнее. ### По самому платежу (рекомендуется) [`POST /v1/payment/info`](/reference/payment-info) возвращает состояние возвратов прямо в объекте платежа: ```python Python theme={null} p = call("/v1/payment/info", {"order_id": "order-1"})["result"] print(p["refund_status"]) # none | partial | full for r in p["refunds"]: # список всех возвратов по этому платежу print(r) ``` ```js Node.js theme={null} const p = (await call("/v1/payment/info", { order_id: "order-1" })).result; console.log(p.refund_status); // none | partial | full for (const r of p.refunds) { // список всех возвратов по этому платежу console.log(r); } ``` | `refund_status` | Значение | | --------------- | ----------------------- | | `none` | Возвратов не было. | | `partial` | Возвращена часть суммы. | | `full` | Возвращено полностью. | Это самый простой способ ответить на вопрос «а мы вообще возвращали деньги по этому заказу и сколько?» — не нужно ничего сопоставлять руками. ### По операции возврата Ответ метода возврата содержит `uuid` операции и её `status` (как у выплаты). Следить за движением конкретной операции можно через [`POST /v1/payout/info`](/reference/payout-info) или по вебхукам — это уровень «дошла ли транзакция в блокчейне». *** ## Автоматический возврат vs ручной Не путайте два механизма: | | Ручной возврат | Автовозврат | | --------- | ------------------------------------------------- | --------------------------------------------------------- | | Метод | [`/v1/payment/refund`](/reference/payment-refund) | [`/v1/payment/autorefund`](/reference/payment-autorefund) | | Когда | Вы решаете вернуть оплаченный платёж. | Автоматически при недоплате/переплате вне допуска. | | Инициатор | Ваш код. | Шлюз. | Про автоматический сценарий — [Недоплата, переплата и автовозврат](/guides/under-overpayment). *** ## Идемпотентность Возврат идемпотентен по тройке `(платёж, адрес, сумма)`. Повтор с той же тройкой вернёт уже созданную операцию, а не отправит средства дважды. Дополнительно отправляйте HTTP‑заголовок `Idempotency-Key` — он защитит от дубля при ретрае после таймаута. → [Устойчивый клиент](/guides/resilient-client#главный-принцип) Массовые возвраты (до 5000 за раз) — [`POST /v1/refund/batch`](/reference/refund-batch), см. [Массовые операции](/guides/batch-operations). *** ## Ошибки | Код | Значение | Что делать | | ------------------------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400 refund.no_address` | Адрес назначения не передан **и** не может быть определён автоматически. | Это Bitcoin/UTXO‑сеть: передайте `address` явно. | | `400 refund.bad_amount` | Некорректная сумма. | Проверьте `amount`; больше оплаченного вернуть нельзя. | | `400 refund.nothing_to_refund` | Возвращать нечего. | Причины: платёж **не оплачен**; уже возвращён полностью; **или это валюто‑агностичный счёт, где покупатель ещё не выбрал монету** — валюты расчёта нет, значит и возвращать нечего. | | `404 payment.not_found` | Платёж не найден. | Сверьте `uuid`/`order_id`. | *** ## Связанные страницы `POST /v1/refund/batch`, до 5000 возвратов. почему возврат влияет на расчёты с партнёрами. # Рецепт устойчивого клиента Source: https://docs.oblodai.com/guides/resilient-client Сеть ненадёжна, а вы работаете с деньгами. Эта инструкция — про то, как построить клиент, который переживает таймауты, повторы и временные сбои, не создавая дублей и не теряя платежи. Здесь сведены воедино идемпотентность, обработка таймаутов, ретраи и backoff. Справочная база — [Идемпотентность](/reference/basics-idempotency) и [Формат ответа и коды ошибок](/reference/basics-errors). *** ## Главный принцип **Каждый денежный запрос отправляйте с заголовком `Idempotency-Key`. Тогда любой повтор безопасен.** Это фундамент всей устойчивости. Без защиты от дублей повтор при таймауте = второй счёт или вторая выплата. С ней повтор = тот же объект. Всё остальное в этой инструкции опирается на это правило. ### Два механизма, а не один Дедупликацию дают **два** независимых механизма. Они работают вместе, и лучше использовать оба. | | HTTP‑заголовок `Idempotency-Key` | Поле `order_id` | | ------------------ | -------------------------------------------------------- | --------------------------------------------- | | Что это | Технический ключ **одной попытки действия** | Ваш **бизнес‑идентификатор** (заказ, выплата) | | Уровень | Транспорт: любой запрос | Данные: платежи, выплаты, возвраты | | Значение | Любое уникальное, ≤255 символов (обычно UUID) | Ваш номер заказа | | Когда генерируется | **До первой отправки**, одинаков во всех попытках | Когда появился заказ | | Что вернёт повтор | **Тот же ответ** + заголовок `Idempotent-Replayed: true` | Тот же объект | | Обязателен? | Нет, но **настоятельно рекомендуется** | Для **выплат — обязателен** | **Ключевое правило заголовка:** значение генерируется **один раз, до первой отправки**, и остаётся тем же во **всех** ретраях этого действия. Если вы сгенерируете новый ключ на каждую попытку — защиты не будет вовсе, вы просто отправите N разных запросов. ```python Python theme={null} import uuid key = str(uuid.uuid4()) # ОДИН РАЗ на действие — не внутри цикла ретраев! for attempt in range(5): resp = call("/v1/payout", { "amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "payout-1", }, idempotency_key=key) # ТОТ ЖЕ ключ во всех попытках ... ``` ```js Node.js theme={null} const key = crypto.randomUUID(); // ОДИН РАЗ на действие — не внутри цикла ретраев! for (let attempt = 0; attempt < 5; attempt++) { const resp = await call("/v1/payout", { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "payout-1", }, key); // ТОТ ЖЕ ключ во всех попытках // ... } ``` Чтобы передать заголовок, добавьте его в свою обёртку `call()` (базовая версия — в инструкции [Как подписать запрос](/guides/signing-requests)): ```python Python theme={null} def call(path: str, payload: dict, idempotency_key: str | None = None) -> dict: body = json.dumps(payload, separators=(",", ":")) ts = str(int(time.time())) signing = f"{ts}\nPOST\n{path}\n{body}" sig = hmac.new(SECRET, signing.encode(), hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, } if idempotency_key: headers["Idempotency-Key"] = idempotency_key # ← в подпись НЕ входит return requests.post(BASE + path, data=body, headers=headers).json() ``` ```js Node.js theme={null} async function call(path, payload, idempotencyKey = null) { const body = JSON.stringify(payload); const ts = Math.floor(Date.now() / 1000).toString(); const signing = `${ts}\nPOST\n${path}\n${body}`; const sig = crypto.createHmac("sha256", SECRET).update(signing).digest("hex"); const headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, }; if (idempotencyKey) { headers["Idempotency-Key"] = idempotencyKey; // ← в подпись НЕ входит } const res = await fetch(BASE + path, { method: "POST", headers, body }); return res.json(); } ``` Заголовок `Idempotency-Key` **не участвует в подписи** — подписывается только каноническая строка `ts\nMETHOD\npath\nbody`. Добавление заголовка ничего не ломает в подписи. ### Что делать, если заголовок ещё не внедрён `order_id` **тоже** дедуплицирует — это ваша страховка. Если вы не готовы менять транспортный слой, минимум остаётся прежним: **всегда задавайте `order_id`**, и повтор вернёт существующий объект. Опасен только случай, когда нет **ни того, ни другого**: без заголовка и без `order_id` повтор создаст дубль. Для выплат это невозможно (`order_id` обязателен), для платежей — вполне. *** ## Проблема таймаута: «ответ не пришёл — операция прошла или нет?» Самая коварная ситуация. Вы отправили `POST /v1/payout`, но ответ не дошёл (таймаут, разрыв). Выплата могла: * **не создаться** — запрос не дошёл до сервера; * **создаться** — сервер обработал, но ответ потерялся по пути к вам. Гадать нельзя. Правильных действий два, и оба безопасны **благодаря идемпотентности**: ### Вариант А. Повторить тот же запрос Повторяйте с **тем же `Idempotency-Key`** и **тем же `order_id`**: ```python Python theme={null} # Повтор идемпотентен: если выплата уже создана — вернётся ОНА, дубля не будет result = call("/v1/payout", { "amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "payout-1", # ТОТ ЖЕ order_id }, idempotency_key=key) # ТОТ ЖЕ Idempotency-Key ``` ```js Node.js theme={null} // Повтор идемпотентен: если выплата уже создана — вернётся ОНА, дубля не будет const result = await call("/v1/payout", { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "payout-1", // ТОТ ЖЕ order_id }, key); // ТОТ ЖЕ Idempotency-Key ``` Если первая попытка прошла — получите ту же выплату (в ответе будет `Idempotent-Replayed: true`). Если не прошла — создастся сейчас. В обоих случаях итог один, и денег не уйдёт дважды. **`409 idempotency.in_progress`** означает, что запрос с этим ключом **прямо сейчас выполняется** на сервере. Это не ошибка и не отказ: подождите и повторите — получите результат первой попытки. ### Вариант Б. Спросить статус ```python Python theme={null} # Узнать, что с выплатой по вашему order_id info = call("/v1/payout/info", {"order_id": "payout-1"}) # 404 payout.not_found → не создавалась, можно создавать # объект выплаты → уже существует, действуйте по её статусу ``` ```js Node.js theme={null} // Узнать, что с выплатой по вашему order_id const info = await call("/v1/payout/info", { order_id: "payout-1" }); // 404 payout.not_found → не создавалась, можно создавать // объект выплаты → уже существует, действуйте по её статусу ``` Для платежей — [`/v1/payment/info`](/reference/payment-info), для выплат — [`/v1/payout/info`](/reference/payout-info). *** ## Почему подписи недостаточно Подпись запроса живёт в окне **±5 минут** — она гасит переигрывание запроса старше пяти минут, но **не** защищает от повторной обработки внутри окна. Реальную защиту от дублей дают `Idempotency-Key` и `order_id`, а не подпись. Не полагайтесь на подпись как на защиту от двойного списания. → [Аутентификация](/reference/basics-auth) *** ## Какие ошибки повторять, а какие — нет | Класс | Коды | Повторять? | Как | | ----------------------- | -------------------------- | ---------------- | ------------------------------------------------- | | Ваша ошибка запроса | `400 *`, большинство `403` | **Нет** | Исправить запрос; повтор даст тот же результат. | | Аутентификация | `401 *` | **Нет** | Починить подпись/часы/ключ. | | Не найдено | `404 *` | **Нет** | Проверить идентификатор. | | Конфликт состояния | `409 *` | **Смотря какой** | **Не** считайте любой `409` финальным — см. ниже. | | Временная недоступность | `503`, `429`-подобная | **Да** | Экспоненциальный backoff. | | Внутренняя | `500` | **Да** | Экспоненциальный backoff. | ### Не все 409 финальны Самая дорогая ошибка в клиенте: трактовать **любой** `409` как финал и бросить операцию. Так вы потеряете законную выплату. Среди `409` есть **ретраибельные**: | Код `409` | Повторять? | Почему | | --------------------------- | -------------- | ---------------------------------------------------------------- | | `payout.funds_maturing` | **ДА, позже** | Средства ещё дозревают. Пройдут подтверждения — выплата пройдёт. | | `idempotency.in_progress` | **ДА, позже** | Запрос с этим `Idempotency-Key` ещё выполняется. | | `payout.frozen` | **ДА, позже** | Сработал kill‑switch, защита временная. | | `payout.insufficient_funds` | **Нет** | Денег реально не хватает — нужно пополнение, а не ожидание. | | Прочие `409` | **Не вслепую** | Сверьтесь через `*/info`. | Полный список кодов — [Справочник кодов ошибок](/reference/errors-catalog). *** ## Особый случай: `409 payout.funds_maturing` Это **не** повод отказаться от выплаты навсегда. Средства ещё дозревают (см. [Модель баланса](/guides/balance-and-funds)). Это временно: повторите позже с backoff — когда депозит наберёт подтверждения, выплата пройдёт. Не путайте с `409 payout.insufficient_funds` (реально не хватает денег — нужно пополнение, а не ожидание). *** ## Экспоненциальный backoff Для `5xx`/`503`/rate limit повторяйте с растущей задержкой, а не в цикле без пауз. Ключевая идея: повторять надо по **HTTP‑статусу** (`429`/`5xx`) и по **сетевым сбоям**, а не только по `error.code`. Здесь `signed_post` / `signedPost` — ваш POST с подписью, возвращающий **сырой** ответ (`requests.Response` в Python, `Response` из fetch в Node.js — в отличие от `call()`, который отдаёт уже разобранное тело), чтобы был виден статус и заголовок `Retry-After`. ```python Python theme={null} import time, random, requests, uuid RETRIABLE_HTTP = {429, 500, 502, 503, 504} # временные статусы # ретраибельные бизнес-коды, приходящие с 4xx (в основном 409) — их НЕЛЬЗЯ хоронить RETRIABLE_CODES = {"payout.funds_maturing", "idempotency.in_progress", "payout.frozen"} def call_resilient(path, payload, max_attempts=6): delay = 1.0 last = None key = str(uuid.uuid4()) # ОДИН ключ на все попытки этого действия for _ in range(max_attempts): try: # signed_post кладёт key в заголовок Idempotency-Key resp = signed_post(path, payload, idempotency_key=key) # → requests.Response except requests.RequestException: # сетевой сбой/таймаут — временно, повторяем (идемпотентность по order_id спасает от дублей) time.sleep(delay); delay = min(delay * 2, 60) + random.uniform(0, 0.5); continue last = resp if 200 <= resp.status_code < 300: # успех return resp.json() if resp.status_code in RETRIABLE_HTTP: # временный статус wait = float(resp.headers.get("Retry-After", delay)) # уважаем Retry-After (напр. 429) time.sleep(min(wait, 60)) delay = min(delay * 2, 60) + random.uniform(0, 0.5) continue # прочие 4xx — не повторяем; ИСКЛЮЧЕНИЕ — ретраибельные бизнес-коды (в т.ч. 409!) body = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {} if body.get("error", {}).get("code") in RETRIABLE_CODES: time.sleep(delay); delay = min(delay * 2, 60) + random.uniform(0, 0.5); continue return body # окончательная ошибка запроса — повтор не поможет if last is None: return None try: return last.json() except ValueError: # не-JSON тело (напр., HTML от прокси на 502) return None ``` ```js Node.js theme={null} const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const RETRIABLE_HTTP = new Set([429, 500, 502, 503, 504]); // ретраибельные бизнес-коды, приходящие с 4xx (в основном 409) const RETRIABLE_CODES = new Set([ "payout.funds_maturing", "idempotency.in_progress", "payout.frozen", ]); // signedPost — ваша подписанная обёртка, возвращающая СЫРОЙ fetch-Response // (а не разобранный JSON), чтобы был виден HTTP-статус и заголовки. См. // «Как подписать запрос» (пример на Node.js). async function callResilient(path, payload, maxAttempts = 6) { let delay = 1000; let last = null; const key = crypto.randomUUID(); // ОДИН ключ на все попытки этого действия for (let attempt = 0; attempt < maxAttempts; attempt++) { let resp; try { resp = await signedPost(path, payload, { idempotencyKey: key }); // → заголовок Idempotency-Key } catch (e) { // сетевой сбой/таймаут — повторяем await sleep(delay); delay = Math.min(delay * 2, 60000) + Math.random() * 500; continue; } last = resp; if (resp.ok) return await resp.json(); // 2xx — успех if (RETRIABLE_HTTP.has(resp.status)) { // 429 + 5xx — временное, ждём и повторяем const ra = parseFloat(resp.headers.get("Retry-After")); // у 429 приходит Retry-After: 60 await sleep(Number.isFinite(ra) ? Math.min(ra * 1000, 60000) : delay); delay = Math.min(delay * 2, 60000) + Math.random() * 500; continue; } // Прочее (4xx) — постоянная ошибка запроса, повтор не поможет. const body = await resp.json().catch(() => ({})); if (RETRIABLE_CODES.has(body.error?.code)) { // ИСКЛЮЧЕНИЕ: ретраибельные 409 await sleep(delay); delay = Math.min(delay * 2, 60000) + Math.random() * 500; continue; } return body; } return last ? await last.json().catch(() => null) : null; } ``` **Почему по HTTP-статусу, а не по `error.code`?** Ответ `429` приходит в особом виде `{"state":1,"message":"rate limit exceeded"}` — **без** `error.code`, поэтому ветвиться на код бесполезно: 429 надо ловить именно по статусу и читать заголовок `Retry-After`. **Джиттер** (случайная добавка к задержке) важен: без него много клиентов после сбоя ударят одновременно. Потолок задержки не даёт ретраям растягиваться бесконечно. *** ## Практические правила * **`Idempotency-Key` генерируется до первой отправки и не меняется при ретраях.** Новый ключ на каждую попытку = никакой защиты. * **`order_id` уникален и стабилен на операцию.** Один заказ → один `order_id`, и при ретраях — тот же. Не генерируйте новый на каждую попытку. * **Таймаут не равен провалу.** Прежде чем считать операцию неудачной, повторите тот же запрос (тот же `Idempotency-Key` и `order_id`) или спросите `*/info`. * **`409` — не повторяйте вслепую, но и не хороните.** `payout.funds_maturing`, `idempotency.in_progress`, `payout.frozen` — повторяйте позже; остальные — сначала выясните состояние через `*/info`. * **Разделяйте временное и постоянное.** `5xx`/`503`/`429`/`funds_maturing` — ждите и повторяйте; `4xx` — чините запрос. * **Храните сопоставление `order_id` ↔ ваш заказ** на своей стороне — это ваш якорь для сверки после любого сбоя. * **Ограничьте число попыток** и логируйте исчерпание — бесконечный ретрай маскирует реальные проблемы. *** ## Как это сочетается с вебхуками Ретраи касаются **исходящих** запросов. Для **входящих** вебхуков действует зеркальный принцип: они доставляются at-least-once, поэтому ваш обработчик тоже должен быть идемпотентным (дедуп по `uuid` + `status`). Устойчивость нужна с обеих сторон. → [Настройка вебхуков](/guides/webhooks-setup) *** ## Связанные страницы # Как подписать запрос Source: https://docs.oblodai.com/guides/signing-requests Подпись HMAC-SHA256 и три заголовка: основа безопасности и источник большинства ошибок на старте. Подпись — основа безопасности API и источник большинства ошибок на старте. Эта инструкция разбирает, как собрать подпись правильно, и показывает типичные грабли. Краткая справочная версия — [Аутентификация](/reference/basics-auth). *** ## Идея за 20 секунд Каждый запрос несёт три заголовка: * `X-Public-Id` — кто вы (несекретный идентификатор ключа); * `X-Timestamp` — когда (unix‑секунды, окно ±5 минут); * `X-Signature` — доказательство, что запрос собрали именно вы, и он не изменён. Подпись — это HMAC‑SHA256 от «канонической строки», склеенной из четырёх частей, на вашем `secret`. *** ## Каноническая строка Соберите строку из четырёх частей, разделённых `\n` (перевод строки): ``` {X-Timestamp}\n{METHOD}\n{path}\n{body} ``` | Часть | Что подставить | | --------------- | ------------------------------------------------------------------------------------------------ | | `{X-Timestamp}` | То же значение, что в заголовке `X-Timestamp`. | | `{METHOD}` | `POST` (в верхнем регистре — других методов у JSON‑эндпоинтов нет). | | `{path}` | Путь запроса, например `/v1/payment`. С query‑строкой, если она есть (у JSON‑эндпоинтов её нет). | | `{body}` | **Точные байты** тела запроса — ровно те, что уйдут в сеть. | Затем: ``` X-Signature = hex( HMAC_SHA256( secret, каноническая_строка ) ) ``` hex в нижнем регистре. *** ## Золотое правило **Сериализуйте тело ОДИН раз в переменную и используйте её и для подписи, и для отправки.** Подпись считается по байтам тела. Если вы подписали один JSON, а отправили другой (пусть даже семантически такой же), подпись не сойдётся и вернётся `401`. *** ## Примеры на четырёх языках ```bash cURL + openssl theme={null} SECRET='oblodai_live_…ваш_секрет' PUBLIC_ID='oblodai_…ваш_public_id' TS=$(date +%s) METHOD='POST' PATHQ='/v1/payment' BODY='{"order_id":"order-1001","currency":"USDT","network":"tron","amount":"25.00"}' SIGSTR=$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATHQ" "$BODY") SIG=$(printf '%s' "$SIGSTR" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') # Опционально: защита от дублей при ретраях. Заголовок в подпись НЕ входит — # просто добавьте к curl: -H "Idempotency-Key: $KEY" (KEY=$(uuidgen)) curl -s https://api.oblodai.com$PATHQ \ -X POST \ -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python highlight={8-11} theme={null} import hmac, hashlib, time, json, requests SECRET = b"oblodai_live_…" PUBLIC_ID = "oblodai_…" BASE = "https://api.oblodai.com" def call(path: str, payload: dict, idempotency_key: str | None = None) -> dict: body = json.dumps(payload, separators=(",", ":")) # компактный JSON — подписываем ровно его ts = str(int(time.time())) signing = f"{ts}\nPOST\n{path}\n{body}" sig = hmac.new(SECRET, signing.encode(), hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, } if idempotency_key: headers["Idempotency-Key"] = idempotency_key # защита от дублей; в подпись НЕ входит r = requests.post(BASE + path, data=body, headers=headers) return r.json() ``` ```js Node.js highlight={8-11} theme={null} import crypto from "node:crypto"; const SECRET = "oblodai_live_…"; const PUBLIC_ID = "oblodai_…"; const BASE = "https://api.oblodai.com"; async function call(path, payload, idempotencyKey = null) { const body = JSON.stringify(payload); // подписываем ровно эту строку const ts = Math.floor(Date.now() / 1000).toString(); const signing = `${ts}\nPOST\n${path}\n${body}`; const sig = crypto.createHmac("sha256", SECRET).update(signing).digest("hex"); const headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, }; if (idempotencyKey) { headers["Idempotency-Key"] = idempotencyKey; // защита от дублей; в подпись НЕ входит } const res = await fetch(BASE + path, { method: "POST", headers, body }); return res.json(); } ``` ```php PHP theme={null} true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, ]); return json_decode(curl_exec($ch), true); } ``` *** ## Частые ошибки и как их поймать | Симптом | Причина | Решение | | ---------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `401` на каждый запрос | Подписали одно тело, отправили другое (библиотека переупорядочила поля или добавила пробелы). | Сериализуйте тело один раз в переменную; подписывайте и отправляйте её же. | | `401 auth.bad_timestamp` | `X-Timestamp` **отсутствует или не число**. | Передайте unix‑секунды. | | `401 merchant.bad_signature` | Подпись не совпала **или** `X-Timestamp` вне окна ±5 минут (проверка окна встроена в сверку подписи). | Проверьте подпись; синхронизируйте часы (NTP). | | `401` только иногда | Разные части кода по‑разному сериализуют тело. | Единая точка сериализации на все запросы. | | `401 auth.ip_not_allowed` | Включён [IP‑allowlist](/reference/api-allowlist), а запрос идёт с другого IP. | Добавьте IP backend в allowlist или временно выключите его. | | Подпись «не та» в вебхуке | Перепутали алгоритм подписи запроса и вебхука. | Для вебхуков — **другой** алгоритм: [Объект вебхука](/reference/webhook-object). | *** ## Помните про идемпотентность Окно подписи ±5 минут не защищает от повторной обработки внутри этого окна. Реальную защиту от дублей дают **два** механизма: * **HTTP‑заголовок `Idempotency-Key`** (рекомендуемый) — любое уникальное значение до 255 символов, одинаковое во всех попытках одного действия. Повтор вернёт тот же ответ и заголовок `Idempotent-Replayed: true`. * **`order_id`/`reference`** — ваш бизнес‑ключ, который тоже дедуплицирует (для выплат обязателен). **Заголовок `Idempotency-Key` в подпись НЕ входит.** Подписывается только каноническая строка `{timestamp}\n{METHOD}\n{path}\n{body}` — добавление заголовка её не меняет и подпись не ломает. Подробнее — [Идемпотентность](/reference/basics-idempotency) и [Рецепт устойчивого клиента](/guides/resilient-client#главный-принцип). *** ## Связанные страницы справочная версия. другой алгоритм подписи. # Сплит‑платежи: автоматическое разделение выручки Source: https://docs.oblodai.com/guides/split-payments Сплит — это правило «с каждого входящего платежа отдавай N % туда‑то». Настроили один раз — дальше доли уходят партнёрам автоматически, без единого вызова API на каждый платёж. Пример: покупатель заплатил **\$100**. У вас настроены два правила — 20 % на адрес партнёра A и 10 % на адрес партнёра B. Итог: **\$70 остаётся вам**, **\$20 уходит A**, **\$10 уходит B**. Вам не нужно ловить вебхук, считать доли и вызывать выплаты — шлюз делает это сам. Справочник методов — [`POST /v1/split/rule`](/reference/split-rule) и [`POST /v1/split/config/get · /set`](/reference/split-config). *** ## Зачем это нужно * **Партнёрские программы** — реферал приводит клиента и получает свой процент с каждой его оплаты. * **Маркетплейс / площадка** — комиссия площадки остаётся вам, остальное уходит продавцу. * **Совместные проекты** — двое соавторов делят выручку 50/50, не сводя счёты вручную. * **Агентские выплаты** — менеджер получает процент со сделок, которые он закрыл. *** ## Шаг 1. Создать правило Правило описывает **одного получателя** и **его долю**. Получателей может быть несколько — создайте несколько правил. Есть два вида назначения, и разница между ними существеннее, чем кажется. ### Вариант А. На внешний адрес ```python Python theme={null} call("/v1/split/rule", { "address": "TXY…", # внешний блокчейн-адрес партнёра "network": "tron", # обязательна вместе с address "percent": 20.0, # 20 % с каждого входящего платежа "note": "Партнёр A", # ваша пометка, чтобы не запутаться }) ``` ```js Node.js theme={null} await call("/v1/split/rule", { address: "TXY…", // внешний блокчейн-адрес партнёра network: "tron", // обязательна вместе с address percent: 20.0, // 20 % с каждого входящего платежа note: "Партнёр A", // ваша пометка, чтобы не запутаться }); ``` Деньги уходят из шлюза наружу, в блокчейн. Обратно их не вернуть. ### Вариант Б. На мерчанта внутри платформы ```python Python theme={null} call("/v1/split/rule", { "merchant_id": "…", # мерчант Oblodai "percent": 10.0, "note": "Партнёр B", }) ``` ```js Node.js theme={null} await call("/v1/split/rule", { merchant_id: "…", // мерчант Oblodai percent: 10.0, note: "Партнёр B", }); ``` Деньги перекладываются **внутри платформы**, с баланса на баланс — движением по внутреннему учёту, без блокчейна. Главное преимущество: такой сплит **обратим**. Если платёж вернут покупателю, доля партнёра будет **отозвана** с его баланса обратно — автоматически и **пропорционально сумме возврата** (вернули половину платежа → с партнёра снимут половину его доли). **Задавайте либо `address` + `network`, либо `merchant_id` — не оба сразу** (иначе `split.bad_destination`). И то и другое одновременно шлюз не примет. | | Внешний адрес | Мерчант внутри платформы | | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | Куда уходит | В блокчейн, наружу | На баланс другого мерчанта | | Комиссия сети | Платится (это реальная транзакция) | Нет | | **Возврат покупателю после раздачи** | **НЕОБРАТИМО.** Долю не отозвать — транзакция в блокчейне окончательна. Возврат вы платите **из своего кармана**. | Доля **отзывается** обратно, пропорционально возврату. | | Когда выбрать | Партнёр вне платформы | Партнёр тоже работает с Oblodai | **Если партнёр есть в платформе — выбирайте `merchant_id`.** Это и дешевле (нет комиссии сети), и принципиально безопаснее при возвратах. Внешний адрес — это дверь в одну сторону. *** ## Шаг 2. Настроить окно удержания (`refund_hold_hours`) Это **самая важная настройка сплитов**. Её нужно понять, а не просто выставить: цена ошибки здесь — ваши собственные деньги. ```python Python theme={null} call("/v1/split/config/set", {"refund_hold_hours": 72}) # держим 72 часа call("/v1/split/config/get", {}) # посмотреть текущее ``` ```js Node.js theme={null} await call("/v1/split/config/set", { refund_hold_hours: 72 }); // держим 72 часа await call("/v1/split/config/get", {}); // посмотреть текущее ``` **Доли уходят партнёрам не сразу.** Сначала платёж «отлёживается» `refund_hold_hours` часов, и только потом происходит расчёт по сплитам. ### Это не бюрократическая задержка, а окно возвратности Вот суть, из которой следует всё остальное: **Возврат покупателю списывает с ВАШЕГО баланса ВСЮ сумму, которую он заплатил.** Не вашу долю после раздачи партнёрам — **все 100 %**. Шлюз не ходит к внешним партнёрам собирать деньги обратно: перевод на внешний адрес — это транзакция в блокчейне, она **необратима**. Значит, пока доли не розданы, возврат безболезнен, а как только розданы — вы платите за него сами. Окно удержания — это ровно **тот промежуток, внутри которого возврат ещё можно провести целиком**. Что происходит **внутри** окна: | Событие внутри окна | Что делает шлюз | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | **Полный возврат** | Сплит **отменяется целиком**. Партнёрам не уходит ничего. Ваш убыток — ноль. | | **Частичный возврат** | Доля партнёра **пересчитывается вниз** пропорционально: вернули половину платежа → партнёр получит половину доли. | | **Возврата не было, окно истекло** | Доли уходят партнёрам. С этого момента платёж «раздан». | Что происходит **после** окна — зависит от вида партнёра: | Возврат после раздачи | Внутренний партнёр (`merchant_id`) | Внешний адрес (`address`) | | --------------------- | ----------------------------------------------------------- | ------------------------- | | Что с долей | **Отзывается** с баланса партнёра, пропорционально возврату | **Не отзывается. Никак.** | | Кто платит возврат | Платформа разруливает по ledger | **Вы, из своего кармана** | ### На цифрах: почему без окна вы уходите в минус Сценарий **без** окна удержания (`refund_hold_hours: 0`), партнёр — на внешнем адресе: ``` Покупатель заплатил $100 Сразу разошлось партнёрам -$30 (20 % + 10 %) → УШЛО В БЛОКЧЕЙН, необратимо У вас на балансе $70 Покупатель просит возврат -$100 ← списывается с ВАС, целиком Итог -$30 ← чистый убыток: вы вернули чужие доли своими деньгами ``` Тот же сценарий **с** окном удержания: ``` Покупатель заплатил $100 → лежит у вас целиком [окно удержания, напр. 72 часа] ├─ Полный возврат? → вернули $100, партнёрам не ушло НИЧЕГО. Убыток $0. ├─ Частичный ($50)? → доля партнёров пересчиталась: вместо $30 уйдёт $15. └─ Возврата не было? → окно закрылось, партнёрам ушло $30, у вас осталось $70. ``` ### `refund_hold_hours: 0` — опасное значение Ноль означает «раздавать доли немедленно». Это **снимает единственную защиту** от описанного выше убытка: возврат по внешнему сплиту становится **невозможно** покрыть из денег партнёра — покрывать будете вы, из своего кармана, на каждом возврате. Ставьте `0` только если вы твёрдо уверены, что возвратов по этим платежам не бывает в принципе. ### Как выбрать значение Ориентируйтесь на **свой** реальный срок возвратов: сколько времени проходит между оплатой и типичной жалобой клиента. Окно должно перекрывать его с запасом. * **Слишком маленькое окно** → партнёры получают деньги раньше, чем вы узнаёте о проблеме. Риск оплатить возврат из своего кармана. * **Слишком большое окно** → партнёры дольше ждут свои деньги и жалуются. Компромисс ищется между этими двумя, но ошибаться безопаснее **в сторону большего окна**: недовольный задержкой партнёр — это разговор, а невозвратный убыток — это деньги. **Сплиты на `merchant_id` мягче к возвратам**: доля отзывается с баланса партнёра даже **после** закрытия окна. Но и там окно полезно — оно избавляет партнёра от «качелей» на балансе (получил → отобрали). *** ## Шаг 3. Проверить, что получилось ```python Python theme={null} rules = call("/v1/split/rule/list", {})["result"] # по каждому правилу видно: назначение, percent, note, rule_id # удалить правило (новые платежи по нему делиться не будут) call("/v1/split/rule/delete", {"rule_id": "…"}) ``` ```js Node.js theme={null} const rules = (await call("/v1/split/rule/list", {})).result; // по каждому правилу видно: назначение, percent, note, rule_id // удалить правило (новые платежи по нему делиться не будут) await call("/v1/split/rule/delete", { rule_id: "…" }); ``` Удаление правила **не** отменяет уже отложенные расчёты по платежам, которые пришли, пока правило действовало. Оно влияет только на **новые** платежи. *** ## Как сплиты влияют на баланс Сплит — это **пятый путь ухода средств** с вашего баланса, наравне с выплатой, автовыводом, возвратом и переводом на личный кошелёк. Отличие в том, что вы его не инициируете: он срабатывает сам, по правилу. Сумма, из которой считается доля, — та, что реально зачислилась. Порядок такой: ``` Покупатель заплатил 100 USDT − комиссия платформы (1.5 % + $0.30) ≈ −1.80 = зачислено на баланс ≈ 98.20 USDT ↓ [окно удержания] − доли партнёров (30 %) = ваша чистая часть ``` Не забывайте про это, планируя экономику: партнёру уходит доля с суммы, а не «сверх» неё — итоговая маржа считается после **обеих** комиссий. → [Модель баланса и движение средств](/guides/balance-and-funds) *** ## Ошибки | Код | Значение | Что делать | | ---------------------------------- | ---------------------------------------------------- | ---------------------------------------------- | | `split.bad_destination` | Заданы и `address`, и `merchant_id` — или ни одного. | Выберите ровно одно назначение. | | `split.network_required` | Задан `address` без `network`. | Укажите сеть адреса. | | `split.bad_percent` | Некорректная доля. | Проверьте `percent`. | | `split.bad_merchant` | Мерчант‑получатель не найден. | Сверьте `merchant_id`. | | `split.self_destination` | Правило указывает на вас самих. | Себе платить не нужно — остаток и так ваш. | | `split.bad_hold` | Некорректное `refund_hold_hours`. | Проверьте значение. | | `split.bad_id` / `split.not_found` | Правило не найдено. | Сверьте `rule_id` через `/v1/split/rule/list`. | | `split.disabled` (`503`) | Сплиты временно выключены. | Временная ошибка — повторите с backoff. | *** ## Связанные страницы окно удержания. сплит как путь ухода средств. почему возврат списывает всю сумму. ручной вывод, если сплит не подходит. # Статические кошельки Source: https://docs.oblodai.com/guides/static-wallets Статический кошелёк — это **постоянный адрес пополнения**. В отличие от инвойса, у него нет фиксированной суммы и срока: любое поступление сразу падает на баланс мерчанта. Идеально для сценария «у каждого клиента свой адрес для пополнения счёта». Справочник объекта — [Объект кошелька](/reference/wallet-object). *** ## Инвойс, платёжная ссылка или статический кошелёк? Способов принять деньги **три**, и выбор между ними определяется двумя вопросами: **кто называет сумму** и **есть ли у вас бэкенд**. | | Инвойс | [Платёжная ссылка](/guides/payment-links) | Статический кошелёк | | ------------------- | ------------------------------------------ | ------------------------------------------------ | ---------------------------------------- | | Метод | [`/v1/payment`](/reference/payment-create) | [`/v1/payment/link`](/reference/payment-link) | [`/v1/wallet`](/reference/wallet-create) | | Сумма | Задаёте вы. | Вы **или покупатель** (`open`/`range`). | Любая, сколько прислали. | | Срок | Ограничен. | Бессрочная или с TTL. | Бессрочный. | | **Нужен ли бэкенд** | **Да**, на каждый заказ. | **Нет** — создали один раз. | **Да**, адрес на клиента. | | Оплата | Закрывает конкретный счёт. | Каждая оплата — свой платёж. | Зачисляется на баланс. | | Комиссия платформы | Удерживается. | Удерживается. | Удерживается (по вашей ставке). | | Вебхук | `invoice.*` (type `payment`). | `invoice.*` (type `payment`). | `wallet.paid` (type `wallet`). | | Сценарий | Разовая покупка в магазине. | Донаты, продажи без сайта, сайт на конструкторе. | Пополнение баланса, депозиты. | Коротко: * **Знаете сумму и есть бэкенд** → инвойс. * **Бэкенда нет** (Tilda/Wix, соцсети) **или сумму называет плательщик** (донаты) → [платёжная ссылка](/guides/payment-links). * **Нужен постоянный адрес за клиентом**, а суммы произвольные → статический кошелёк (эта страница). *** ## Персональный адрес для клиента Тройка `(currency, network, order_id)` идемпотентна. Передайте `order_id` = идентификатор клиента — и получите его **персональный постоянный адрес**. Повторный вызов с той же тройкой вернёт тот же адрес. ```python Python theme={null} def deposit_address(client_id: str) -> str: w = call("/v1/wallet", { "currency": "USDT", "network": "tron", "order_id": client_id, # закрепляет адрес за клиентом })["result"] return w["address"] # Один и тот же клиент всегда получит один и тот же адрес addr = deposit_address("client-42") ``` ```js Node.js theme={null} async function depositAddress(clientId) { const w = (await call("/v1/wallet", { currency: "USDT", network: "tron", order_id: clientId, // закрепляет адрес за клиентом })).result; return w.address; } // Один и тот же клиент всегда получит один и тот же адрес const addr = await depositAddress("client-42"); ``` *** ## Приём пополнений Каждое поступление на кошелёк порождает вебхук `wallet.paid`: ```json theme={null} { "type": "wallet", "uuid": "0e5b6b9a-…", "order_id": "client-42", "address": "TXk9...c3Fd", "network": "tron", "currency": "USDT", "payment_amount": "150.00", "status": "paid", "is_final": true } ``` Обрабатывайте его так же, как платёжный вебхук: проверьте подпись, дедуплицируйте по `uuid` + `status`. См. [Настройка вебхуков](/guides/webhooks-setup) и [Объект вебхука](/reference/webhook-object). **`payment_amount` — это сумма, пришедшая на адрес (брутто), ДО нашей комиссии.** На ваш баланс зачисляется **нетто** — за вычетом комиссии платформы (по умолчанию \~1.5%). Если вы кредитуете конечного клиента, начисляйте ему нетто (или заранее заложите комиссию в свою экономику), иначе начислите больше, чем реально легло на баланс. Точную удержанную сумму сверяйте по своему балансу ([`POST /v1/balance`](/reference/balance)). *** ## Блокировка и возврат Если нужно перестать принимать на адрес — заблокируйте его: ```python Python theme={null} call("/v1/wallet/block", {"address": "TXk9...c3Fd"}) # заблокировать call("/v1/wallet/block", {"address": "TXk9...c3Fd", "is_force_block": False}) # разблокировать ``` ```js Node.js theme={null} await call("/v1/wallet/block", { address: "TXk9...c3Fd" }); // заблокировать await call("/v1/wallet/block", { address: "TXk9...c3Fd", is_force_block: false }); // разблокировать ``` `is_force_block` по умолчанию `true`. Чтобы **снять** блокировку, явно передайте `false`. Средства, полученные на (обычно заблокированный) кошелёк, можно вывести на один адрес через [`POST /v1/wallet/blocked-address-refund`](/reference/wallet-blocked-refund) — вернётся чистая сумма за вычетом газа. *** ## Нюансы * **Комиссия платформы удерживается и с пополнений на статический кошелёк** — по вашей ставке (по умолчанию \~1.5%, та же, что и на инвойсе). На баланс попадает нетто. Поэтому «гонять» продажи через статические кошельки ради экономии на комиссии смысла нет: ставка та же. Статические кошельки — для пополнений/депозитов (напр. балансы пользователей); для разовых продаж используйте инвойс ([`/v1/payment`](/reference/payment-create)) — он даёт сумму, срок и статус конкретного заказа. * Пара `(currency, network)` должна быть [поддерживаемым методом](/reference/basics-networks). * Для каждого клиента используйте **уникальный** `order_id` — иначе они будут делить один адрес. *** ## Связанные страницы третий способ принять оплату, без бэкенда. инвойс. # Тестирование интеграции Source: https://docs.oblodai.com/guides/testing **У Oblodai нет отдельной песочницы (sandbox) и тестовых ключей.** API работает на боевом окружении. Это значит, что реальные платежи и выплаты двигают реальные средства. Тестировать интеграцию нужно осознанно — эта инструкция показывает, как проверить максимум логики **без** риска и без реальных переводов, а что придётся проверять малыми реальными суммами. *** ## Что можно проверить без единого перевода Часть флоу тестируется полностью бесплатно и безопасно: | Что проверяем | Чем | Двигает деньги? | | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | --------------------- | | Подпись запроса работает | любой чтение‑эндпоинт, напр. [`/v1/balance`](/reference/balance) | нет | | Приёмник вебхуков: доставка, парсинг, ответ 2xx (подпись — только на боевом платеже) | [тестовые вебхуки](/reference/webhooks-test) | нет | | Доступность методов приёма/выплат | [`/v1/payment/services`](/reference/payment-services), [`/v1/payout/services`](/reference/payout-services) | нет | | Расчёт комиссии выплаты | [`/v1/payout/calculate`](/reference/payout-calculate) | нет | | Курсы валют | [`/v1/exchange-rate/list`](/reference/exchange-rate-list) | нет | | Создание счёта и получение адреса | [`/v1/payment`](/reference/payment-create) | нет (пока не оплачен) | | Настройки (accuracy, autorefund, discount, accepted, fee‑config) | соответствующие `*/get`·`/set` | нет | **Создание счёта денег не двигает** — вы получаете адрес и все поля ответа, не оплачивая. Это уже покрывает большую часть кода: сериализацию тела, подпись, разбор ответа, сохранение `uuid`. *** Самый первый тест — убедиться, что подпись собирается правильно. Достаточно любого чтения: ```python Python theme={null} print(call("/v1/balance", {})) # Успех → {"state": 0, "result": {...}} — подпись верна # 401 → разбираемся: см. «Как подписать запрос» ``` ```js Node.js theme={null} console.log(await call("/v1/balance", {})); // Успех → {"state": 0, "result": {...}} — подпись верна // 401 → разбираемся: см. «Как подписать запрос» ``` Если тут `401` — не идите дальше, чините подпись. → [Как подписать запрос](/guides/signing-requests) Не дожидаясь реальной оплаты, отправьте пробное событие на свой URL: ```python Python theme={null} resp = call("/v1/test-webhook/payment", { "url_callback": "https://shop.example/oblodai/callback", "status": "paid", "currency": "USDT", "network": "tron", }) print(resp["result"]) # {"result": true, "status_code": 200} ``` ```js Node.js theme={null} const resp = await call("/v1/test-webhook/payment", { url_callback: "https://shop.example/oblodai/callback", status: "paid", currency: "USDT", network: "tron", }); console.log(resp.result); // {"result": true, "status_code": 200} ``` `status_code` — то, чем ответил ваш обработчик. Нужен `200`. **Пробные тела не подписаны** и содержат `"is_test": true`. Ваш код проверки подписи должен уметь их пропускать (или тестируйте подпись отдельно). Готовый дружелюбный к тестам обработчик — в [Отладке вебхуков](/guides/webhooks-debug). Так проверяется весь путь доставки: доходит ли запрос, парсится ли тело, отвечаете ли `2xx`. Не проверяется только **реальная подпись** — её отработаете на первом боевом платеже. Создайте счёт и убедитесь, что разбираете ответ: ```python Python theme={null} inv = call("/v1/payment", { "amount": "1", "currency": "USD", "order_id": "test-001", "to_currency": "USDT", "network": "tron", })["result"] assert inv["address"] # адрес выделен assert inv["payment_status"] == "check" print(inv["url"]) # ссылка на страницу оплаты — можно открыть глазами ``` ```js Node.js theme={null} import assert from "node:assert"; const inv = (await call("/v1/payment", { amount: "1", currency: "USD", order_id: "test-001", to_currency: "USDT", network: "tron", })).result; assert(inv.address); // адрес выделен assert(inv.payment_status === "check"); console.log(inv.url); // ссылка на страницу оплаты — можно открыть глазами ``` Проверьте **оба** механизма идемпотентности — они независимы, и сломаться может любой: 1. **Заголовок `Idempotency-Key`.** Отправьте один и тот же запрос дважды с одним и тем же значением заголовка — второй ответ должен быть **идентичен** первому и прийти с заголовком `Idempotent-Replayed: true`. Так вы убеждаетесь, что ваш клиент действительно **не генерирует новый ключ на каждую попытку** (самая частая ошибка внедрения). 2. **`order_id`.** Повторите создание счёта с тем же `order_id` — должен вернуться **тот же** счёт, а не создаться второй. → [Идемпотентность](/reference/basics-idempotency) · [Устойчивый клиент](/guides/resilient-client#главный-принцип) Оплату «живой» транзакцией, подтверждения сети, реальную подпись вебхука и выплату полностью проверить синтетикой нельзя. Делайте это **малыми суммами на дешёвой сети**: * **Выберите дешёвую сеть.** Tron (`tron`), Polygon (`polygon`), BSC (`bsc`) — низкие комиссии сети. Избегайте Ethereum и Bitcoin для тестов: дорого и есть минимальные суммы. * **Помните про минимум сети.** На дорогих сетях платёж ниже минимума вернёт `payment.below_minimum`. На дешёвых минимума обычно нет. * **Проверьте боевую подпись вебхука** именно на этом реальном платеже — это единственный способ убедиться, что алгоритм и секрет верны. → [Объект вебхука](/reference/webhook-object) * **Тест выплаты:** сначала [`/v1/payout/calculate`](/reference/payout-calculate) (денег не двигает — видите комиссию и итог), затем реальная выплата малой суммы на свой же адрес. Помните: выплаты по API‑ключу **необратимы** и уходят сразу. * **Тест возврата/автовозврата:** сделайте недоплату/переплату малой суммой и проверьте, что статусы (`wrong_amount`/`paid_over`) и автовозврат отрабатывают. → [Недоплата и переплата](/guides/under-overpayment) Отдельно протестируйте сценарий, который часто всплывает уже в проде: сразу после оплаты средства на балансе, но ещё **не выводимы** (maturity‑холд). Попробуйте вывести их сразу — должны получить `409 payout.funds_maturing`. Убедитесь, что ваш код это корректно обрабатывает и не считает ошибкой навсегда. → [`POST /v1/balance`](/reference/balance) *** ## Дисциплина боевого тестирования Раз песочницы нет, заведите правила, чтобы боевые тесты не смешивались с настоящими заказами: * **Префикс для тестовых `order_id`** (например `test-...`) — легко отфильтровать и не спутать с реальными заказами. * **Отдельный тестовый адрес‑получатель** для выплат, который вы контролируете. * **Минимально возможные суммы** и дешёвые сети. * **Логируйте `public_id`, `uuid`, `order_id`** каждого теста — потом проще свести концы. * **Не коммитьте ключи** — даже во временном тестовом скрипте. *** ## Чек‑лист тестирования * Подпись запроса верна (любой чтение‑эндпоинт вернул `state: 0`). * Идемпотентность (заголовок): повтор с тем же `Idempotency-Key` вернул тот же ответ и `Idempotent-Replayed: true`; клиент **не** генерирует новый ключ на каждую попытку. * Идемпотентность (`order_id`): повтор создания счёта с тем же `order_id` вернул тот же счёт. * Приёмник вебхуков принимает тестовое событие и отвечает `status_code: 200`. * Обработчик пропускает `is_test`‑тела и проверяет подпись боевых. * Реальная подпись вебхука проверена на одном малом платеже. * `payout/calculate` возвращает ожидаемую комиссию. * Малая реальная выплата на свой адрес прошла до статуса `paid`. * `409 payout.funds_maturing` обрабатывается корректно — клиент **повторяет** выплату позже, а не считает её проваленной. * Недоплата/переплата и автовозврат проверены малой суммой. *** ## Связанные страницы # Что делать, если не работает — диагностика Source: https://docs.oblodai.com/guides/troubleshooting Единая точка входа, когда «что‑то пошло не так». Найдите свой **симптом** в таблице и переходите к разделу с пошаговой диагностикой. Короткие ответы на частые вопросы — в [FAQ](/guides/faq-troubleshooting); здесь — подробнее и по шагам. **Первое правило диагностики:** ветвитесь по **коду** ошибки (`error.code`), а не по тексту. Полный список кодов с расшифровкой и что делать — [Справочник кодов ошибок](/reference/errors-catalog). *** ## Быстрая навигация по симптому | Симптом | Куда | | ---------------------------------------- | ---------------------------------------------------------------------- | | Каждый запрос отдаёт `401` | [Ошибки аутентификации](#ошибки-аутентификации-401/403) | | `429` / «rate limit» | [Превышен лимит частоты](#превышен-лимит-частоты-429) | | Платёж создался, но `address` пустой | [Платёж: пустой адрес](#платёж-создался-но-address-пустой) | | Покупатель оплатил, а статус не меняется | [Диагностика: платёж «застрял»](#диагностика-платёж-«застрял») | | Вебхук вообще не приходит | [Диагностика: вебхук не приходит](#диагностика-вебхук-не-приходит) | | Вебхук приходит, но подпись не сходится | [Вебхук: подпись не сходится](#вебхук-приходит-но-подпись-не-сходится) | | Выплата не проходит (`payout.*`) | [Выплаты не проходят](#выплаты-не-проходят) | | В магазине (CMS) заказ не оплачивается | [Модули CMS](#модули-cms-заказ-не-меняет-статус) | | Запрос завис / таймаут / `5xx` | [Таймауты и недоступность API](#таймауты-и-недоступность-api) | *** ## Сначала — 5 быстрых проверок Прежде чем углубляться, проверьте базовое (это закрывает большинство проблем на старте): 1. **Ключи вставлены без пробелов** по краям (частая причина `401`). 2. **Секрет — это секрет, а не public\_id** (их легко перепутать местами). 3. **Сумма — строкой** (`"10.00"`), а не числом. 4. **Сайт доступен из интернета** (для вебхуков — `localhost` не подойдёт). 5. **Вы на боевом URL** API, а не на плейсхолдере из документации. *** ## Ошибки аутентификации (401/403) **Симптом:** любой запрос отдаёт `401`. Проверьте по порядку: 1. **`X-Public-Id` и `secret`.** Скопируйте заново, без пробелов. Секрет — тот, что подписывает, а не `public_id`. 2. **Строка для подписи.** Подписывается ровно `"{timestamp}\n{METHOD}\n{path}\n{body}"`, а `X-Signature = hex(HMAC‑SHA256(secret, строка))`. Тело подписи должно **побайтно** совпадать с отправляемым телом — не пересериализуйте JSON после подписи. Разбор — [Как подписать запрос](/guides/signing-requests). 3. **Часы сервера.** `timestamp` — в **unix‑секундах**, окно ±5 минут. Разъехались часы → синхронизируйте NTP. Симптом окна — код `merchant.bad_signature` (а не `auth.bad_timestamp`). 4. **`merchant.wrong_key_kind` (403)** — ключ не подходит для этого эндпоинта. **Второго ключа искать не надо:** в Oblodai **один API‑ключ на всё** — и приём, и выплаты (раздельных ключей «для приёма» и «для выплат» не существует). На практике этот код означает, что вы предъявляете **легаси‑ключ** старого формата (`pk_…` / `wk_…`). Выпустите в кабинете актуальный ключ `oblodai_…` и используйте его везде. → [Регистрация и ключи](/guides/get-keys) 5. **`401 auth.ip_not_allowed`** — включён [IP‑allowlist](/reference/api-allowlist), а запрос идёт с адреса вне списка. Добавьте IP или временно выключите контроль. Проще всего — не подписывать вручную, а взять [SDK](/sdk/overview): он не ошибётся в подписи. *** ## Превышен лимит частоты (429) **Симптом:** `429` с телом `{"state":1,"message":"rate limit exceeded"}`. * Лимит считается **на IP** (по умолчанию 120 запросов/мин). Подробнее — [Ограничение частоты](/reference/basics-ratelimit). * **Что делать:** прочитать заголовок `Retry-After` (секунды), подождать и повторить (повтор безопасен благодаря идемпотентности: тот же `Idempotency-Key` + тот же `order_id`). SDK делают это сами. * **Как не упираться:** * **Массовые операции отправляйте батчем** — [`/v1/payment/batch`](/reference/payment-batch), `/v1/refund/batch`, `/v1/payout/batch`: до **5000** операций **одним** запросом вместо 5000 запросов. Это штатный способ вообще не встречаться с `429`. → [Массовые операции](/guides/batch-operations) * Не поллите `/payment/info` — статус придёт вебхуком. * Кэшируйте `services`/`currencies`/курсы. См. [best‑practices](/reference/basics-ratelimit#как-не-упереться-в-лимит). *** ## Платёж: создался, но `address` пустой Это **нормально** для валюто‑агностичного счёта (`is_multi: true`): валюту и сеть выбирает покупатель на [hosted‑странице](/reference/pay-select), и адрес выделяется в момент выбора. * Хотите адрес сразу — задайте `to_currency` **и** `network` при создании (одновалютный режим). Разбор режимов — [Три режима создания счёта](/guides/payment-modes). *** ## Платёж не создаётся | Код | Что значит | Что делать | | ---------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `400 payment.to_currency_required` | Цена в **фиате**, задан `network`, но не `to_currency` — монету расчёта вывести нельзя. | Укажите `to_currency` или уберите `network`. → [Три режима создания счёта](/guides/payment-modes#частая-ошибка-цена-в-фиате-и-сеть-без-монеты) | | `400 payment.network_required` | У монеты несколько сетей, а `network` не задан. | Укажите `network` явно. | | `400 payment.unknown_currency` | Валюта не поддерживается (частый случай — фиат вне списка: **KZT, KGS, UZS не поддерживаются**). | Сверьтесь с `pricing_currencies` в [`GET /v1/currencies`](/reference/currencies). | | `400 payment.below_minimum` | Сумма ниже минимума сети (Ethereum, Bitcoin). | Увеличьте сумму или возьмите дешёвую сеть. | | `503 invoice.fee_quote_failed` | **Временная** ошибка: не удалось получить котировку курса/комиссии. | Повторите с backoff (тот же `Idempotency-Key`) — это не ошибка вашего запроса. → [Устойчивый клиент](/guides/resilient-client) | *** ## Диагностика: платёж «застрял» **Симптом:** покупатель говорит, что оплатил, а статус заказа не стал `paid`. Идите по шагам: 1. **Запросите факт.** Вызовите [`/v1/payment/info`](/reference/payment-info) по `uuid`/`order_id` и посмотрите `payment_status`: * `check` / `select` — оплаты ещё **не видели** на блокчейне. Ждём или платёж не отправлен. * `confirm_check` — транзакцию **видим, ждём подтверждений сети** (это нормально, подождите). * `wrong_amount_waiting` — пришла **недоплата**, покупатель может **докинуть остаток** (см. [Недоплата/переплата](/guides/under-overpayment)). * `paid` / `paid_over` — оплата **прошла**. Значит проблема на вашей стороне: не пришёл/не обработался вебхук → см. [следующий раздел](#диагностика-вебхук-не-приходит). * `wrong_amount` — недоплата, срок вышел. `cancel` — счёт истёк/отменён. 2. **Если `paid`, а заказ не обновился** — проблема в доставке/обработке вебхука. Проверьте [журнал доставок](/reference/webhooks-deliveries) и раздел ниже. 3. **Разовая ручная синхронизация:** можно [переотправить вебхук](/reference/payment-resend) или обновить заказ по результату `/payment/info` вручную. *** ## Диагностика: вебхук не приходит **Симптом:** оплата в статусе `paid`, но уведомление на ваш сервер не пришло. Пройдите по цепочке — уведомление рвётся в одном из звеньев: 1. **Endpoint зарегистрирован?** Oblodai шлёт вебхуки **только** если вы вызвали [`POST /v1/webhooks`](/reference/webhooks-register) и получили `secret`. Без регистрации доставок нет вовсе — даже per‑объектный `url_callback` игнорируется (без секрета endpoint подписывать нечем, доставка не создаётся). (Модули CMS делают это автоматически при сохранении настроек.) 2. **URL доступен снаружи?** Адрес должен открываться из интернета (не `localhost`, не за VPN, не за Basic‑Auth). Проверьте, что на ваш `url_callback` можно сделать `POST` извне. 3. **Ваш сервер отвечает `2xx`?** Если обработчик возвращает не‑2xx (или падает) — Oblodai считает доставку неуспешной и будет **повторять** (до 12 попыток, \~3,5 часа), затем статус `dead`. 4. **Смотрите журнал доставок:** [`POST /v1/webhooks/deliveries`](/reference/webhooks-deliveries) — там видно `status` (`pending`/`delivered`/`dead`), число попыток и текст последней ошибки. Учтите: журнал хранит только **50 последних** доставок и не имеет фильтров (тело запроса — пустой `{}`) — на бойком магазине нужная запись могла выпасть из окна, и её отсутствие **не** означает, что вебхук не отправлялся. В этом случае сверьте статус платежа через [`/v1/payment/info`](/reference/payment-info) и при необходимости [переотправьте вебхук](/reference/payment-resend). 5. **Проверьте вручную:** [тестовый вебхук](/reference/webhooks-test) отправит событие на ваш URL — так можно отделить «Oblodai не шлёт» от «мой сервер не принимает». Подробная отладка — [Отладка вебхуков](/guides/webhooks-debug). *** ## Вебхук приходит, но подпись не сходится Частые причины (по убыванию частоты): 1. **Проверяете не тем секретом.** Вебхуки подписываются секретом из [`POST /v1/webhooks`](/reference/webhooks-register), а **не** вашим API‑секретом. 2. **Не сырое тело.** Подпись считается по **сырым байтам** запроса. Если фреймворк распарсил JSON, а вы пересобрали его в строку — подпись не сойдётся. Берите `php://input` / `express.raw` / `request.get_data()`. 3. **Другой алгоритм.** Подпись вебхука — это `hex(HMAC‑SHA256(secret, "{timestamp}." + сырое_тело))` по заголовкам `X-Webhook-Timestamp` / `X-Webhook-Signature`. Это **не** тот же алгоритм, что у подписи запроса. Формат — [Объект вебхука](/reference/webhook-object). 4. **Пробный вебхук.** События с `"is_test": true` **не подписаны** — их не нужно проверять, просто отвечайте `200`. *** ## Выплаты не проходят | Код | Что значит | Что делать | | ---------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `payout.insufficient_funds` (409) | Не хватает **доступного** баланса | Денег реально нет: пополните баланс. Повтор **не** поможет. Часть средств может быть заморожена/дозревает — проверьте [баланс](/reference/balance). | | `payout.funds_maturing` (409) | Средства ещё **дозревают** (maturity‑холд) | **ПОВТОРИТЕ ПОЗЖЕ.** Это временная ошибка, а не отказ — когда депозит наберёт подтверждения, выплата пройдёт. Не отменяйте выплату из-за неё. | | `payout.frozen` (409) | Выплаты **заморожены** (kill‑switch) | Временная защита; **повторите позже**. | | `payout.no_destination` / `payout.bad_address` | Пустой/некорректный адрес | Проверьте адрес получателя. | | `payout.address_network_mismatch` | Адрес не соответствует сети | Сверьте адрес и `network`. | | `payout.order_id_required` | Не передан `order_id` | Для выплаты `order_id` **обязателен** — это ваш бизнес‑ключ дедупликации (дополняющий заголовок `Idempotency-Key`). | **Не все `409` финальны.** Частая и дорогая ошибка — считать любой `409` конечным («уже обработано») и бросить операцию. `payout.funds_maturing`, `payout.frozen` и `idempotency.in_progress` **надо повторять позже** — иначе вы потеряете законную выплату. → [Устойчивый клиент](/guides/resilient-client#не-все-409-финальны) Полный список — [Справочник кодов ошибок](/reference/errors-catalog), раздел `payout`. *** ## Модули CMS: заказ не меняет статус 1. Убедитесь, что способ оплаты **включён** и виден на оформлении заказа. 2. **Сохраните настройки способа оплаты ещё раз** — так модуль перерегистрирует вебхук. 3. Сайт должен быть **доступен из интернета** (иначе вебхуки не дойдут). 4. Проверьте, что **Public ID** и **API secret** вставлены без пробелов. *** ## Таймауты и недоступность API Сеть моргнула, запрос завис или пришёл `5xx`/`503` — это ожидаемая ситуация, к ней надо быть готовым. * **Таймаут ≠ «не прошло».** Ответ мог потеряться уже после того, как операция выполнилась на сервере. Поэтому **не создавайте вторую операцию вслепую** — повторите **тот же** запрос: с тем же HTTP‑заголовком `Idempotency-Key` и тем же `order_id`. Идемпотентность вернёт уже созданный объект (с заголовком `Idempotent-Replayed: true`), а не сделает дубль. * **`5xx` / `503`** — временная недоступность зависимости (например, оракула курсов). Повторяйте с **экспоненциальным backoff** (0.5с → 1с → 2с …), а не в плотном цикле. * **`409 idempotency.in_progress`** — не ошибка: ваш предыдущий запрос с этим `Idempotency-Key` ещё выполняется. Подождите и повторите — получите его результат. * **Что повторять безопасно:** запросы с `Idempotency-Key` и/или `order_id`/`reference` (создание платежа, выплаты, возврата) — идемпотентны. Чтения (`info`, `balance`, `services`) безопасны всегда. * **Готовый рецепт клиента** (таймауты, ретраи, backoff, идемпотентность) — [Устойчивый клиент](/guides/resilient-client). Все [SDK](/sdk/overview) реализуют это из коробки. *** ## Не нашли решение? * Быстрые вопросы‑ответы — [FAQ](/guides/faq-troubleshooting). * Все коды ошибок и что делать — [Справочник кодов ошибок](/reference/errors-catalog). * Как это устроено «под капотом» — [Диаграммы жизненных циклов](/reference/diagrams). * Незнакомый термин — [Глоссарий](/reference/glossary). * Ничего не помогло — создайте тикет в поддержку в кабинете [my.oblodai.com](https://my.oblodai.com) (раздел **«Поддержка»**). Приложите ваш `public_id`, `uuid` платежа/выплаты и строки лога с префиксом `oblodai:` (в SDK включаются переменной `OBLODAI_LOG`, в модулях CMS — галочкой **Debug log**); `secret` **не** прикладывайте. # Недоплата, переплата и автовозврат Source: https://docs.oblodai.com/guides/under-overpayment В крипте покупатель почти никогда не отправляет ровно ту сумму, что вы выставили: комиссия сети, округление в кошельке, задержка курса. Oblodai даёт две настройки, которые определяют, что считать успешной оплатой и что делать с расхождением: **допуск** (`accuracy`) и **автовозврат** (`autorefund`). *** ## Две настройки, две разные задачи | Настройка | Отвечает на вопрос | Метод | | -------------------------- | ---------------------------------------------------- | --------------------------------------------------------- | | `accuracy` (допуск) | «Какое отклонение от суммы всё ещё считать оплатой?» | [`/v1/payment/accuracy`](/reference/payment-accuracy) | | `autorefund` (автовозврат) | «Что вернуть, если платёж вышел за допуск?» | [`/v1/payment/autorefund`](/reference/payment-autorefund) | Они работают вместе: сначала `accuracy` решает, засчитать ли платёж; то, что в допуск не попало, обрабатывает `autorefund`. *** ## Допуск (accuracy) Если включить допуск, счёт, отклонившийся от суммы (недоплата или переплата) **не более чем на N %**, всё равно станет `paid`. ```python Python theme={null} # Разрешить недоплату до 2% call("/v1/payment/accuracy/set", {"enabled": True, "accuracy_percent": 2}) ``` ```js Node.js theme={null} // Разрешить недоплату до 2% await call("/v1/payment/accuracy/set", { enabled: true, accuracy_percent: 2 }); ``` * Диапазон `accuracy_percent` — 1–5, кэп 5 %. * **По умолчанию допуск \~1 %, а не выключен.** Если вы ничего не настраивали, счёт с недоплатой/переплатой в пределах \~1 % засчитается как `paid`. Чтобы требовать **ровную** сумму (нулевой допуск), нужно **явно** сохранить настройку выключенной: `call("/v1/payment/accuracy/set", {"enabled": False})`. * Для конкретного счёта можно перекрыть настройку полем `accuracy_payment_percent` в [`POST /v1/payment`](/reference/payment-create). **Пример:** выставили 10 USDT, допуск 2 %. Пришло 9.85 USDT — это в пределах 2 %, счёт станет `paid`. Пришло 9.50 USDT — вне допуска: счёт **не** закрывается сразу, а ждёт (`wrong_amount_waiting`) на случай доплаты, и лишь по **истечении срока** становится `wrong_amount` (недоплата). *** ## Автовозврат (autorefund) Что делать с расхождением, которое **не** попало в допуск: ```python Python theme={null} # Возвращать и переплату, и истёкшую недоплату call("/v1/payment/autorefund/set", {"overpay": True, "underpay": True}) ``` ```js Node.js theme={null} // Возвращать и переплату, и истёкшую недоплату await call("/v1/payment/autorefund/set", { overpay: true, underpay: true }); ``` * `overpay: true` — при переплате (`paid_over`) вернуть **излишек**. * `underpay: true` — при истёкшей недоплате (`wrong_amount`) вернуть **полученное**. * Оба флага по умолчанию включены. Возврат идёт на **адрес плательщика**. **По умолчанию из возвращаемой суммы удерживаются и наша комиссия, и сетевой газ** (авто-возвраты по умолчанию «за счёт клиента»): плательщик получает `сумма − наша комиссия − газ`. Кто платит комиссию возврата — можно переключить на уровне проекта ([refund-fee-config](/reference/payout-refund-fee-config)). Поддержаны EVM, Tron, TON, Solana. Для **Bitcoin/UTXO** возврат ручной — через [`POST /v1/payment/refund`](/reference/payment-refund). *** ## Как статусы связаны с настройками | Пришло | Статус | Если `accuracy` покрывает | Что делает `autorefund` | | ---------------------------------------- | -------------- | ------------------------- | ---------------------------------- | | Ровно или чуть больше в пределах допуска | `paid` | — | ничего | | Меньше, но в пределах допуска | `paid` | засчитано как оплата | ничего | | Меньше, срок вышел, вне допуска | `wrong_amount` | — | вернёт при `underpay: true` | | Больше сверх допуска | `paid_over` | — | вернёт излишек при `overpay: true` | *** ## Ручное решение по недоплате: `/v1/payment/resolve` Автовозврат — политика «по умолчанию». Для конкретного недоплаченного счёта (`wrong_amount`) решение можно принять вручную — методом [`POST /v1/payment/resolve`](/reference/payment-resolve): * **`action: "accept"`** — принять частичную оплату: средства остаются у вас, автовозврат для этого счёта отключается. Удобно, когда недоплата копеечная или вы договорились с покупателем. * **`action: "refund"`** — вернуть недоплату плательщику прямо сейчас, не дожидаясь поллера (или когда автовозврат выключен). ```python Python theme={null} call("/v1/payment/resolve", {"order_id": "ord-1001", "action": "accept"}) ``` ```js Node.js theme={null} await call("/v1/payment/resolve", { order_id: "ord-1001", action: "accept" }); ``` Решение одно на счёт: повторный `accept` — no-op, противоположное действие вернёт `resolution.already_resolved`. Если покупатель успел доплатить и счёт стал `paid`, придёт `resolution.not_underpaid` — сверьтесь с `/v1/payment/info`. *** ## Рекомендации * **Включите небольшой допуск** (1–2 %) — это резко снижает число «недоплат» из‑за комиссий и округления, при которых покупатель по факту заплатил почти всё. * **Держите автовозврат включённым**, чтобы не разбирать расхождения вручную. Помните про ручной возврат для Bitcoin/UTXO. * **Разрешайте доплату** (`is_payment_multiple: true` при создании счёта), если хотите принимать недоплату частями до истечения срока. *** ## Связанные страницы # Отладка вебхуков Source: https://docs.oblodai.com/guides/webhooks-debug Прежде чем принимать боевой трафик, убедитесь, что ваш приёмник вебхуков действительно работает. Oblodai даёт инструменты, чтобы отправить пробное событие и посмотреть журнал доставок — без ожидания реальной оплаты. *** ## Отправить пробное событие Пробный вебхук синхронно шлёт тело на ваш URL и возвращает HTTP‑код, которым ответил ваш эндпоинт. ```python Python theme={null} resp = call("/v1/test-webhook/payment", { "url_callback": "https://shop.example/oblodai/callback", "status": "paid", "currency": "USDT", "network": "tron", }) print(resp["result"]) # {"result": true, "status_code": 200} ``` ```js Node.js theme={null} const resp = await call("/v1/test-webhook/payment", { url_callback: "https://shop.example/oblodai/callback", status: "paid", currency: "USDT", network: "tron", }); console.log(resp.result); // {"result": true, "status_code": 200} ``` `status_code` — то, что вернул ваш обработчик. Если это не `200`/`2xx`, значит приёмник не принял событие: смотрите свои логи. Есть три типа: `/v1/test-webhook/payment`, `/wallet`, `/payout`. Легаси‑форма — `testing-webhook`. Полностью — [Тестовые вебхуки](/reference/webhooks-test). **Пробные тела НЕ подписаны** и содержат `"is_test": true`. Если ваш код строго проверяет подпись, он отклонит пробное тело — это ожидаемо. Тестируйте либо с временным пропуском проверки для `is_test`, либо проверяйте подпись отдельно на боевых событиях. *** ## Пример: обработчик, дружелюбный к тестам ```python Python theme={null} @app.post("/oblodai/callback") def callback(): raw = request.get_data() event = request.get_json(silent=True) or {} if event.get("is_test"): # пробное тело — не подписано; просто подтверждаем приём return "ok", 200 # боевое тело — проверяем подпись ts = request.headers.get("X-Webhook-Timestamp", "") sig = request.headers.get("X-Webhook-Signature", "") expected = hmac.new(WEBHOOK_SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): return "bad signature", 403 handle_event(event) return "ok", 200 ``` ```js Node.js theme={null} app.post("/oblodai/callback", express.raw({ type: "*/*" }), (req, res) => { const raw = req.body.toString("utf8"); let event = {}; try { event = JSON.parse(raw); } catch {} if (event.is_test) { // пробное тело — не подписано; просто подтверждаем приём return res.send("ok"); } // боевое тело — проверяем подпись const ts = req.get("X-Webhook-Timestamp") || ""; const sig = req.get("X-Webhook-Signature") || ""; const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(`${ts}.${raw}`).digest("hex"); const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)); if (!ok) { return res.status(403).send("bad signature"); } handleEvent(event); res.send("ok"); }); ``` *** ## Посмотреть журнал доставок Последние доставки (до 50) с их статусом и последней ошибкой: ```python Python theme={null} d = call("/v1/webhooks/deliveries", {})["result"]["deliveries"] for x in d: print(x["event_type"], x["status"], x["attempts"], x["last_error"]) ``` ```js Node.js theme={null} const d = (await call("/v1/webhooks/deliveries", {})).result.deliveries; for (const x of d) { console.log(x.event_type, x.status, x.attempts, x.last_error); } ``` Статусы доставки: | Статус | Значение | | ----------- | -------------------------- | | `pending` | В очереди или ждёт ретрая. | | `delivered` | Ваш endpoint вернул `2xx`. | | `dead` | Исчерпаны все попытки. | Полностью — [`POST /v1/webhooks/deliveries`](/reference/webhooks-deliveries). *** ## Переотправить вебхук платежа Если доставка ушла в `dead` (например, ваш сервер лежал), поставьте текущий вебхук платежа в очередь заново: ```python Python theme={null} call("/v1/payment/resend", {"order_id": "order-1001"}) ``` ```js Node.js theme={null} await call("/v1/payment/resend", { order_id: "order-1001" }); ``` Событие соответствует **текущему** статусу платежа. Помните: переотправка создаёт **ещё одну** доставку — обработчик должен быть идемпотентен (дедуп по `uuid` + `status`). См. [`POST /v1/payment/resend`](/reference/payment-resend). *** ## Чек‑лист отладки * Endpoint зарегистрирован ([`/v1/webhooks`](/reference/webhooks-register)), секрет сохранён. * Пробное событие доходит и возвращает `status_code: 200`. * Обработчик берёт **сырое тело** (не пересериализованное) для проверки подписи. * Боевая подпись проверяется корректно (алгоритм вебхука, не запроса). * Повторная доставка того же события не приводит к двойной выдаче (дедуп по `uuid` + `status`). * `2xx` возвращается только после успешной обработки. * В журнале доставок статус `delivered`, а не `pending`/`dead`. *** ## Связанные страницы # Безопасность вебхуков за пределами подписи Source: https://docs.oblodai.com/guides/webhooks-security Проверка подписи вебхука — необходимый минимум, но не полная защита. Эта инструкция про то, что кусает уже в проде и стоит денег: защиту от повторного проигрывания (replay), перепроверку статуса перед выдачей ценного, и оставшиеся острые углы приёма вебхуков. Базовая проверка подписи описана в [Настройке вебхуков](/guides/webhooks-setup) и [Объекте вебхука](/reference/webhook-object) — здесь мы идём глубже и не повторяем её. *** ## Что подпись НЕ гарантирует Верная подпись доказывает две вещи: тело пришло от Oblodai и не изменено в пути. Она **не** гарантирует: * что это уведомление **свежее**, а не перехваченное и проигранное заново позже (replay); * что описанное в теле состояние **всё ещё актуально** к моменту, когда вы его обрабатываете; * что вы не обрабатываете это событие **повторно** (доставка at-least-once). Каждый пункт закрывается отдельно. *** ## 1. Replay-защита по timestamp В каждой доставке есть заголовок `X-Webhook-Timestamp` — момент отправки в unix-секундах, и он **входит в подписанную строку** (`hex(HMAC-SHA256(secret, "{timestamp}." + сырое_тело))`). Значит, timestamp нельзя подменить, не сломав подпись. Используйте это: **отклоняйте вебхуки со слишком старым timestamp**. Тогда даже валидно подписанное, но перехваченное ранее уведомление нельзя будет проиграть спустя время. ```python Python theme={null} import time, hmac, hashlib MAX_WEBHOOK_AGE = 300 # 5 минут — подберите под свою терпимость def verify_fresh(secret: bytes, ts_header: str, raw_body: bytes, sig_header: str) -> bool: # 1) подпись expected = hmac.new(secret, ts_header.encode() + b"." + raw_body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig_header): return False # 2) свежесть try: age = abs(time.time() - int(ts_header)) except ValueError: return False return age <= MAX_WEBHOOK_AGE ``` ```js Node.js theme={null} import crypto from "node:crypto"; const MAX_WEBHOOK_AGE = 300; function verifyFresh(secret, tsHeader, rawBody, sigHeader) { const expected = crypto.createHmac("sha256", secret).update(`${tsHeader}.${rawBody}`).digest("hex"); const a = Buffer.from(expected), b = Buffer.from(sigHeader || ""); // timingSafeEqual бросает исключение на разной длине — сверяем длину сами. if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false; const age = Math.abs(Date.now() / 1000 - Number(tsHeader)); return Number.isFinite(age) && age <= MAX_WEBHOOK_AGE; } ``` **Про окно и ретраи.** `X-Webhook-Timestamp` **обновляется на каждой попытке доставки** — при ретрае Oblodai переподписывает тело со свежей меткой времени. Поэтому окно `300` секунд **не** отбраковывает легитимные повторы (даже если сам ретрай случился через час): каждая доставка «свежая» на момент отправки. `300` c — разумная отправная точка; сужать окно ради «защиты от ретраев» не нужно. *** ## 2. Дедупликация — защита от повторной обработки Доставка **at-least-once**: одно событие может прийти несколько раз (в том числе после [`/v1/payment/resend`](/reference/payment-resend) или ретраев). Дедуплицируйте по паре `uuid` + `status` и обрабатывайте повтор как no-op. ```python Python theme={null} def handle(event): key = (event["uuid"], event["status"]) if seen(key): # ваша БД/кэш return # уже обработано — идемпотентный no-op process(event) mark_seen(key) ``` ```js Node.js theme={null} function handle(event) { const key = `${event.uuid}:${event.status}`; if (seen(key)) { // ваша БД/кэш return; // уже обработано — идемпотентный no-op } processEvent(event); markSeen(key); } ``` Ключ должен жить в **надёжном** хранилище (БД), а не в памяти процесса — иначе после рестарта дедуп сбросится. Порядок доставок не гарантирован: опирайтесь на `status`/`is_final`, а не на очерёдность. *** ## 3. Перепроверка статуса перед выдачей ценного Вебхук — сигнал «сходи и проверь», а для дорогих действий (отгрузка товара, крупное зачисление, разблокировка услуги) — не единственный источник правды. Перед необратимой выдачей **перепроверьте состояние авторитетным запросом**: ```python Python theme={null} def on_paid_webhook(event): # не доверяем телу вебхука на 100% для дорогой операции — # спрашиваем актуальный статус у API info = call("/v1/payment/info", {"uuid": event["uuid"]})["result"] if info["payment_status"] in ("paid", "paid_over"): fulfil_order(info["order_id"]) # идемпотентно ``` ```js Node.js theme={null} async function onPaidWebhook(event) { // не доверяем телу вебхука на 100% для дорогой операции — // спрашиваем актуальный статус у API const info = (await call("/v1/payment/info", { uuid: event.uuid })).result; if (["paid", "paid_over"].includes(info.payment_status)) { await fulfilOrder(info.order_id); // идемпотентно } } ``` Это закрывает случаи, когда состояние изменилось между отправкой вебхука и его обработкой у вас, и добавляет второй независимый барьер к проверке подписи. Для мелких/некритичных событий такой шаг можно опустить — решайте по цене ошибки. *** ## 4. Приёмник: острые углы * **Только HTTPS, публичный адрес.** При регистрации URL проходит SSRF-проверку (приватные/локальные адреса запрещены). → [`/v1/webhooks`](/reference/webhooks-register) * **Сырое тело до парсинга.** Подпись считается по байтам; фреймворк, который пересериализует JSON, сломает проверку (`express.json()` на роуте вебхука — частая ошибка). * **Секрет вебхука — это секрет.** Храните его как ключ API. Помните: повторный вызов [`/v1/webhooks`](/reference/webhooks-register) **меняет** URL и выдаёт **новый** секрет — старый перестанет подходить. * **Пробные тела не подписаны** (`is_test: true`). Ваш код должен их отличать и не проверять подписью — но и не выполнять по ним реальную выдачу. → [Отладка вебхуков](/guides/webhooks-debug) * **`2xx` только после успешной обработки.** Иначе Oblodai повторит доставку (это фича, не баг). * **Отвечайте быстро, обрабатывайте асинхронно.** Тяжёлую работу выносите в очередь, а вебхуку отвечайте `2xx` сразу после того, как надёжно приняли событие — так вы не упрётесь в таймауты доставки и ретраи. *** ## Мини-чек-лист безопасности вебхуков * Подпись проверяется по **сырому телу** правильным алгоритмом (timestamp + `.` + тело). * Секрет — из [`/v1/webhooks`](/reference/webhooks-register), не ключ API. * Проверяется **свежесть** по `X-Webhook-Timestamp` (replay-защита). * Дедуп по `uuid` + `status` в надёжном хранилище (не в памяти). * Для дорогих операций — перепроверка статуса через `*/info`. * Пробные тела (`is_test`) отсекаются от реальной выдачи. * `2xx` возвращается только после успешной обработки. * Endpoint только HTTPS, тяжёлая работа — асинхронно. *** ## Связанные страницы # Настройка и приём вебхуков Source: https://docs.oblodai.com/guides/webhooks-setup Вебхуки — как Oblodai сообщает вашему серверу, что платёж оплачен, кошелёк пополнен или выплата подтверждена. Это правильный способ узнавать о событиях — надёжнее, чем постоянный опрос. Разберём регистрацию, проверку подписи и обязательную идемпотентность. Справочник формата — [Объект вебхука](/reference/webhook-object). ## Что такое вебхук — на пальцах **Вебхук** — это HTTP‑запрос (`POST`), который сервер Oblodai **сам присылает вам**, когда что‑то произошло (например, счёт оплачен). **Endpoint** (он же `url_callback`, он же «callback URL») — это **ваш собственный** адрес и обработчик, которые вы должны **написать**: маршрут на вашем сервере, принимающий этот `POST`. То есть вебхук работает так: покупатель оплатил → Oblodai делает `POST` на ваш URL → ваш код принимает его и помечает заказ оплаченным. **На CMS‑модуле это уже сделано.** Если вы используете готовый CMS-модуль (WooCommerce и т. п.) — регистрировать вебхук и писать обработчик **не нужно**, модуль делает всё сам. Инструкция ниже — для собственной интеграции на SDK/HTTP. ## Что нужно подготовить до регистрации 1. **Публичный HTTPS‑URL**, доступный из интернета, отвечающий на `POST`. Пример в коде ниже (`https://shop.example/oblodai/callback`) — **подставьте свой** реальный адрес. 2. **Ваш обработчик должен возвращать HTTP `2xx`** (обычно `200`). Любой не‑2xx (или падение) Oblodai считает неудачной доставкой и будет **повторять**. 3. **Локальная разработка:** на `localhost` вебхуки **не дойдут** (и SSRF‑проверка запретит приватные адреса). Поднимите **туннель** — [ngrok](https://ngrok.com/download) (`ngrok http 8000` → адрес `https://xxxx.ngrok-free.app`), Cloudflare Tunnel или localtunnel — и регистрируйте выданный публичный HTTPS‑адрес. *** Один раз на проект зарегистрируйте HTTPS‑URL и сохраните выданный `secret`. В примерах `call()` — подписанная обёртка запроса (её определение — в [Как подписать запрос](/guides/signing-requests); проще взять [SDK](/sdk/overview)): ```python Python theme={null} resp = call("/v1/webhooks", {"url": "https://shop.example/oblodai/callback"}) webhook_secret = resp["secret"] # ПОКАЗЫВАЕТСЯ ОДИН РАЗ — сохраните в секрет-хранилище ``` ```js Node.js theme={null} const resp = await call("/v1/webhooks", { url: "https://shop.example/oblodai/callback" }); const webhookSecret = resp.secret; // ПОКАЗЫВАЕТСЯ ОДИН РАЗ — сохраните в секрет-хранилище ``` Этот эндпоинт — единственный, кто отвечает `201 Created` **без** конверта `state`/`result`. Особенности: * У проекта **один** активный endpoint. Повторный вызов заменит URL и выдаст **новый** секрет. * URL проходит SSRF‑проверку: приватные и локальные адреса запрещены. * Индивидуальный `url_callback` конкретного платежа/выплаты доставляется на свой URL, но **подписывается секретом endpoint проекта** — то есть endpoint всё равно должен быть зарегистрирован. Полностью — [`POST /v1/webhooks`](/reference/webhooks-register). **Подпись вебхука — ДРУГОЙ алгоритм**, не тот, что у подписи запроса. Здесь это `timestamp` + точка `.` + **сырое тело**. Никаких метода/пути/переводов строк. ``` X-Webhook-Signature = hex( HMAC_SHA256( secret, "{X-Webhook-Timestamp}." + сырое_тело ) ) ``` Возьмите **сырое тело** запроса (до JSON‑парсинга) и сравните подписи в постоянном времени. ```python Python theme={null} # пример на Flask import hmac, hashlib from flask import Flask, request app = Flask(__name__) WEBHOOK_SECRET = b"b7c1e9…" @app.post("/oblodai/callback") def callback(): raw = request.get_data() # СЫРОЕ тело ts = request.headers.get("X-Webhook-Timestamp", "") sig = request.headers.get("X-Webhook-Signature", "") expected = hmac.new(WEBHOOK_SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): return "bad signature", 403 event = request.get_json() handle_event(event) # см. следующий шаг return "ok", 200 ``` ```js Node.js theme={null} // пример на Express import express from "express"; import crypto from "node:crypto"; const app = express(); const WEBHOOK_SECRET = "b7c1e9…"; // ВАЖНО: берём сырое тело, не express.json() app.post("/oblodai/callback", express.raw({ type: "*/*" }), (req, res) => { const raw = req.body.toString("utf8"); const ts = req.get("X-Webhook-Timestamp") || ""; const sig = req.get("X-Webhook-Signature") || ""; const expected = crypto.createHmac("sha256", WEBHOOK_SECRET).update(`${ts}.${raw}`).digest("hex"); const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)); if (!ok) { return res.status(403).send("bad signature"); } handleEvent(JSON.parse(raw)); // см. следующий шаг res.send("ok"); }); ``` ```php PHP theme={null} Доставка — **как минимум один раз**. Один и тот же вебхук может прийти несколько раз (в том числе после [`payment/resend`](/reference/payment-resend)). Обязательно: * **Дедуплицируйте по `uuid` + `status`.** Если такое событие уже обработано — верните `2xx` и выйдите (no‑op). * **Не полагайтесь на порядок доставок.** Опирайтесь на `status`/`is_final`, а не на очерёдность. * **Отвечайте `2xx` только после успешной обработки.** Любой не‑2xx — сигнал повторить доставку. ```python Python theme={null} def handle_event(event): key = (event["uuid"], event["status"]) if already_processed(key): # ваша дедупликация (БД/кэш) return if event["type"] == "payment" and event["status"] == "paid": mark_order_paid(event["order_id"]) elif event["type"] == "wallet" and event["status"] == "paid": credit_client(event["order_id"], event["payment_amount"]) elif event["type"] == "payout": update_payout(event["uuid"], event["status"]) mark_processed(key) ``` ```js Node.js theme={null} function handleEvent(event) { const key = `${event.uuid}:${event.status}`; if (alreadyProcessed(key)) { // ваша дедупликация (БД/кэш) return; } if (event.type === "payment" && event.status === "paid") { markOrderPaid(event.order_id); } else if (event.type === "wallet" && event.status === "paid") { creditClient(event.order_id, event.payment_amount); } else if (event.type === "payout") { updatePayout(event.uuid, event.status); } markProcessed(key); } ``` *** ## Какие события приходят | `X-Webhook-Event` | Поле `type` | Когда | | ------------------------------------------------------------------ | ----------- | --------------------------------------------- | | `invoice.paid` | `payment` | Платёж оплачен в допуске. | | `invoice.paid_over` | `payment` | Переплата. | | `invoice.wrong_amount` | `payment` | Недоплата, срок вышел. | | `invoice.expired` | `payment` | Счёт истёк. | | `wallet.paid` | `wallet` | Пополнение статик‑кошелька. | | `payout.<статус>` (`payout.confirmed` — успех, `payout.failed`, …) | `payout` | События выплаты; суффикс — внутренний статус. | *** ## Ретраи Если ваш endpoint не ответил `2xx`, диспетчер повторяет доставку с экспоненциальным backoff: от 10 с с удвоением, потолок 1 час, до 12 попыток (в сумме \~3,5 часа). Исчерпав попытки — статус `dead`, виден в [журнале доставок](/reference/webhooks-deliveries). *** ## Проверьте себя до боя Прогоните пробное событие, не дожидаясь реальной оплаты — см. [Отладка вебхуков](/guides/webhooks-debug). *** ## Связанные страницы replay‑защита, перепроверка статуса. # Oblodai — криптоплатёжный шлюз Source: https://docs.oblodai.com/index Приём криптоплатежей, выплаты, статические кошельки и вебхуки. Один API-ключ — весь функционал. Здесь есть всё, чтобы встроить **Oblodai** в ваш продукт: пошаговые [Инструкции](/guides/overview), точный [Справочник API](/reference/overview) и готовые [SDK](/sdk/overview) для пяти языков. **API-версия:** `v1` · **Базовый URL:** `https://api.oblodai.com` · **Hosted-оплата:** `https://pay.oblodai.com` ## С чего начать Зарегистрируйтесь в кабинете [my.oblodai.com](https://my.oblodai.com) и получите `public_id` + `secret`. Подробнее — в разделе [Регистрация и ключи](/guides/get-keys). Пишете свой бэкенд — возьмите [SDK](/sdk). Хотите разобраться в API — начните с [Быстрого старта](/quickstart). Пройдите [Быстрый старт](/quickstart) — от ключей до первого вебхука за пять шагов. ## Разделы документации Пошаговые руководства: приём платежей, выплаты, вебхуки, переход с Heleket. Идите сюда, когда нужно решить задачу. Точное описание каждого объекта и метода API. Что принимает, что возвращает, какие ошибки. Примеры на cURL, Python и Node.js. Готовые библиотеки для PHP, TypeScript/Node.js, Python, Go и Rust. Подпись, проверка вебхуков и повторы — из коробки. ## Выберите свой путь Подключите SDK для вашего языка и вызовите пару методов. Быстрый старт, подпись запросов и сквозной пример приложения. ## Ключевые принципы за 30 секунд * **Один API-ключ на всё.** Единый ключ мерчанта аутентифицирует и приём платежей, и выплаты. Радиус поражения ограничен одним вашим мерчантом; за хранение ключа отвечаете вы. * **Всё server-to-server.** Секрет ключа никогда не попадает в браузер или мобильное приложение — подпись считается только на вашем сервере. * **Каждый запрос подписывается** HMAC-SHA256; аутентификацию несут три заголовка (`X-Public-Id`, `X-Timestamp`, `X-Signature`). См. [Аутентификацию](/reference/basics-auth). * **Идемпотентность — заголовком `Idempotency-Key`.** Одинаковый ключ во всех попытках → повтор вернёт тот же результат, а не создаст второй платёж. Без заголовка и без `order_id` повтор создаст дубль. См. [Идемпотентность](/reference/basics-idempotency). * **Валюта цены ≠ валюта расчёта.** `currency` — в чём назначена цена (23 фиатные валюты или любая монета), `to_currency` — в чём идёт расчёт: всегда только крипта. Баланс, выплаты и возвраты тоже всегда в крипте. * **Суммы — строками, ответ — конвертом.** Суммы передаются строками в единицах валюты (`"25.00"`, `"0.015"`), без float. Успех — конверт `state: 0` + `result`, ошибка — объект `error`. Не нашли нужное? Загляните в [Справочник кодов ошибок](/reference/errors-catalog) или [Глоссарий](/reference/glossary). Что-то не работает — [диагностика по симптому](/guides/troubleshooting). # Быстрый старт Source: https://docs.oblodai.com/quickstart От ключей до первого вебхука за пять шагов. За пять шагов вы примете первый платёж: получите ключи, подпишете запрос, зарегистрируете URL для вебхуков, создадите счёт и поймаете вебхук об оплате. **Песочницы нет — платежи настоящие.** У Oblodai нет тестового режима: «первый платёж» ниже — это **реальная** оплата на маленькую сумму, которую вы отправите сами себе. Что можно проверить без реальных переводов — см. [Тестирование](/guides/testing). Примеры кода даны на Python и Node.js (переключайтесь вкладками), но подпись и вызовы идентичны на любом языке (см. [Как подписать запрос](/guides/signing-requests) для cURL / PHP). *** ## Подготовка ### Что понадобится * **Аккаунт и ключи** — зарегистрируйтесь в кабинете и получите `public_id` + `secret`. Подробно — [Регистрация и ключи](/guides/get-keys). * **Сервер/бэкенд**, где будет считаться подпись (секрет не должен попадать в браузер). * **Публично доступный HTTPS‑URL для вебхуков.** На своём ноутбуке (`localhost`) вебхуки **не дойдут**. На время разработки поднимите **туннель**: программу, которая даёт вашему локальному серверу временный публичный HTTPS‑адрес. Самый простой — [ngrok](https://ngrok.com/download): установили, запустили `ngrok http 8000` — получили адрес вида `https://xxxx.ngrok-free.app`, его и указывайте при регистрации вебхука в шаге 3 (поле `url`). Альтернативы: Cloudflare Tunnel, localtunnel. ## Пять шагов Зарегистрируйтесь в кабинете [my.oblodai.com](https://my.oblodai.com) и создайте API‑ключ (пошагово — [Регистрация и ключи](/guides/get-keys)). Вы получите: * **`public_id`** — несекретный идентификатор. Можно логировать. Уходит в заголовке `X-Public-Id`. * **`secret`** — секрет для подписи. **Показывается один раз** — сохраните его в секрет‑хранилище. Один ключ работает и для приёма, и для выплат. Подробнее — [Аутентификация](/reference/basics-auth). Каждый запрос подписывается HMAC‑SHA256 по канонической строке из четырёх частей. Вот минимальная функция‑обёртка, которую мы будем переиспользовать: ```python Python highlight={9-12} theme={null} import hmac, hashlib, time, json, os, requests # Ключи читаем из окружения — не хардкодьте их в код. SECRET = os.environ["OBLODAI_SECRET"].encode() # секрет API-ключа PUBLIC_ID = os.environ["OBLODAI_PUBLIC_ID"] # oblodai_… / oblodai_live_… BASE = "https://api.oblodai.com" def call(path: str, payload: dict, idempotency_key: str | None = None) -> dict: body = json.dumps(payload, separators=(",", ":")) # подписываем ровно эту строку ts = str(int(time.time())) signing = f"{ts}\nPOST\n{path}\n{body}" sig = hmac.new(SECRET, signing.encode(), hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, } if idempotency_key: headers["Idempotency-Key"] = idempotency_key # защита от дублей; в подпись НЕ входит r = requests.post(BASE + path, data=body, headers=headers) return r.json() ``` ```js Node.js highlight={9-12} theme={null} import crypto from "node:crypto"; // Ключи читаем из окружения — не хардкодьте их в код. const SECRET = process.env.OBLODAI_SECRET; // секрет API-ключа const PUBLIC_ID = process.env.OBLODAI_PUBLIC_ID; // oblodai_… / oblodai_live_… const BASE = "https://api.oblodai.com"; async function call(path, payload, idempotencyKey = null) { const body = JSON.stringify(payload); // подписываем ровно эту строку const ts = Math.floor(Date.now() / 1000).toString(); const signing = `${ts}\nPOST\n${path}\n${body}`; const sig = crypto.createHmac("sha256", SECRET).update(signing).digest("hex"); const headers = { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, }; if (idempotencyKey) { headers["Idempotency-Key"] = idempotencyKey; // защита от дублей; в подпись НЕ входит } const res = await fetch(BASE + path, { method: "POST", headers, body }); return res.json(); } ``` Детальный разбор и типичные ошибки — [Как подписать запрос](/guides/signing-requests). Один раз на проект зарегистрируйте endpoint и **сохраните `secret`** — им проверяется подпись входящих вебхуков (это **другой** секрет, чем ключ API): ```python Python theme={null} # Внимание: этот эндпоинт возвращает "голый" объект без конверта state/result # "url" — ваш ПУБЛИЧНЫЙ HTTPS-адрес из «Что понадобится» (напр. # https://xxxx.ngrok-free.app/oblodai/callback), не localhost resp = call("/v1/webhooks", {"url": "https://shop.example/oblodai/callback"}) webhook_secret = resp["secret"] # ПОКАЗЫВАЕТСЯ ОДИН РАЗ — сохраните ``` ```js Node.js theme={null} // Внимание: этот эндпоинт возвращает "голый" объект без конверта state/result // "url" — ваш ПУБЛИЧНЫЙ HTTPS-адрес из «Что понадобится» (напр. // https://xxxx.ngrok-free.app/oblodai/callback), не localhost const resp = await call("/v1/webhooks", { url: "https://shop.example/oblodai/callback" }); const webhookSecret = resp.secret; // ПОКАЗЫВАЕТСЯ ОДИН РАЗ — сохраните ``` Подробнее — [`POST /v1/webhooks`](/reference/webhooks-register). ```python Python theme={null} payment = call("/v1/payment", { "amount": "10", "currency": "USD", "order_id": "order-1", "to_currency": "USDT", "network": "tron", "lifetime": 3600, })["result"] print(payment["address"]) # адрес, на который платит покупатель print(payment["url"]) # ссылка на hosted-страницу оплаты ``` ```js Node.js theme={null} const payment = (await call("/v1/payment", { amount: "10", currency: "USD", order_id: "order-1", to_currency: "USDT", network: "tron", lifetime: 3600, })).result; console.log(payment.address); // адрес, на который платит покупатель console.log(payment.url); // ссылка на hosted-страницу оплаты ``` Покажите покупателю `address` (или отправьте его на `url`). Полное описание полей ответа — [Объект платежа](/reference/payment-object). Когда покупатель оплатит, на ваш URL придёт вебхук `invoice.paid`. Обработчик должен: 1. Взять **сырое тело** запроса и заголовки `X-Webhook-Timestamp`, `X-Webhook-Signature`. 2. Проверить подпись секретом из шага 3 (из ответа `POST /v1/webhooks`, **не** API-secret). 3. Дедуплицировать по `uuid` + `status` и ответить `2xx` **только после** успешной обработки. ```python Python theme={null} import hmac, hashlib from flask import Flask, request app = Flask(__name__) WEBHOOK_SECRET = b"b7c1e9…" # секрет из ответа POST /v1/webhooks (шаг 3), НЕ API-secret @app.post("/oblodai/callback") def callback(): raw = request.get_data() # сырое тело — ВАЖНО не пересериализовывать event = request.get_json(silent=True) or {} # Пробный вебхук из /guides/testing приходит с "is_test": true и БЕЗ подписи — # пропускаем его ДО проверки подписи, иначе ответим 403 if event.get("is_test"): return "ok", 200 ts = request.headers.get("X-Webhook-Timestamp", "") sig = request.headers.get("X-Webhook-Signature", "") expected = hmac.new(WEBHOOK_SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): return "bad signature", 403 if event.get("status") == "paid": # ... пометить заказ event["order_id"] оплаченным (идемпотентно) pass return "ok", 200 ``` ```js Node.js theme={null} import crypto from "node:crypto"; import express from "express"; const app = express(); const WEBHOOK_SECRET = "b7c1e9…"; // секрет из ответа POST /v1/webhooks (шаг 3), НЕ API-secret app.post("/oblodai/callback", express.raw({ type: "*/*" }), (req, res) => { const raw = req.body; // сырое тело (Buffer) — ВАЖНО не пересериализовывать let event = {}; try { event = JSON.parse(raw.toString("utf8")); } catch { /* ignore */ } // Пробный вебхук из /guides/testing приходит с "is_test": true и БЕЗ подписи — // пропускаем его ДО проверки подписи, иначе ответим 403 if (event.is_test) return res.send("ok"); const ts = req.get("X-Webhook-Timestamp") ?? ""; const sig = req.get("X-Webhook-Signature") ?? ""; const expected = crypto.createHmac("sha256", WEBHOOK_SECRET) .update(ts + ".").update(raw).digest("hex"); const ok = sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig)); if (!ok) { return res.status(403).send("bad signature"); } if (event.status === "paid") { // ... пометить заказ event.order_id оплаченным (идемпотентно) } return res.status(200).send("ok"); }); ``` Подробно про формат, заголовки и идемпотентность — [Объект вебхука](/reference/webhook-object) и [Настройка вебхуков](/guides/webhooks-setup). ## Связанные страницы тот же поток, но с разбором каждого статуса. фиксированная валюта, авто‑сеть, агностичный счёт. вывести средства с баланса. перед боевым трафиком. диагностика типичных проблем на старте. # IP-allowlist Source: https://docs.oblodai.com/reference/api-allowlist **Методы:** `/v1/api-allowlist/list` · `/add` · `/remove` · `/enable` Пер‑мерчантский список доверенных IP/CIDR. Когда включён, подписанные запросы с IP вне списка отклоняются (`401 auth.ip_not_allowed`). По умолчанию выключен. **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## POST /v1/api-allowlist/list Тело — `{}`. ```json theme={null} { "state": 0, "result": { "entries": ["203.0.113.10", "198.51.100.0/24"], "enabled": true } } ``` *** ## POST /v1/api-allowlist/add IP‑адрес или CIDR‑подсеть. Ошибка: `400 apiallow.bad_cidr`. *** ## POST /v1/api-allowlist/remove IP или CIDR для удаления. *** ## POST /v1/api-allowlist/enable Включить/выключить контроль. Ошибка: `400 apiallow.empty` — нельзя включить контроль с пустым списком; сначала добавьте хотя бы один IP. *** ## Пример: добавить IP и включить ```bash cURL theme={null} # 1. Добавить IP BODY='{"cidr":"203.0.113.10"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/api-allowlist/add' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/api-allowlist/add \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" -d "$BODY" # 2. Включить контроль BODY='{"enabled":true}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/api-allowlist/enable' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/api-allowlist/enable \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" -d "$BODY" ``` ```python Python theme={null} call("/v1/api-allowlist/add", {"cidr": "203.0.113.10"}) call("/v1/api-allowlist/enable", {"enabled": True}) ``` ```js Node.js theme={null} await call("/v1/api-allowlist/add", { cidr: "203.0.113.10" }); await call("/v1/api-allowlist/enable", { enabled: true }); ``` *** ## Нюансы * **Порядок важен:** нельзя включить контроль с пустым списком (`apiallow.empty`). Сначала добавьте IP вашего backend, потом включайте — иначе рискуете заблокировать сами себя. * Запросы с IP вне включённого списка получают `401 auth.ip_not_allowed`. *** ## Связанные страницы # Автовывод Source: https://docs.oblodai.com/reference/auto-withdraw **Методы:** `/v1/auto-withdraw/list` · `/set` · `/delete` Автовывод. Как только депозит зачисляется мерчанту в настроенном активе, **чистая** сумма (gross − наша комиссия) автоматически выплачивается на указанный адрес. **Автовывод vs выплата.** Автовывод **автоматически** отправляет **чистую** поступившую сумму на заданный адрес по мере зачисления; [`/v1/payout`](/reference/payout-create) — это **ручной разовый** вывод на произвольный адрес по вашей команде. **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). Одно правило на пару `(мерчант, актив)`, выплата **авто‑одобряется**. Механизм best‑effort и идемпотентный; суммы ниже `min_minor` остаются на балансе. Адрес проходит комплаенс‑скрининг. *** ## POST /v1/auto-withdraw/list Тело — пустой объект `{}` без параметров. ```json theme={null} { "state": 0, "result": { "rules": [ { "currency": "USDT", "network": "tron", "address": "T…", "min_minor": "5000000" } ] } } ``` `min_minor` — порог в **минимальных единицах**; `"0"` = без порога. *** ## POST /v1/auto-withdraw/set Актив, для которого включается автовывод. Сеть. Адрес назначения. Порог **в единицах актива** (не minor). По умолчанию `0` — без порога. ### Пример запроса ```bash cURL theme={null} BODY='{"currency":"USDT","network":"tron","address":"T…","min":"5"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/auto-withdraw/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/auto-withdraw/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/auto-withdraw/set", {"currency": "USDT", "network": "tron", "address": "T…", "min": "5"}) ``` ```js Node.js theme={null} await call("/v1/auto-withdraw/set", { currency: "USDT", network: "tron", address: "T…", min: "5" }); ``` ### Коды ошибок | Код | Значение | | ------------------------------------- | -------------------------------------- | | `400 autowithdraw.unknown_currency` | Неизвестный актив. | | `400 autowithdraw.bad_min` | Некорректный порог. | | `400 autowithdraw.missing` | Не хватает обязательного поля. | | `400 payout.bad_address` | Пустой адрес назначения. | | `400 payout.address_network_mismatch` | Адрес не соответствует выбранной сети. | *** ## POST /v1/auto-withdraw/delete Выключает автовывод для актива. Актив, для которого выключается автовывод. *** ## Нюансы **Единицы порога легко перепутать.** В `/set` порог задаётся полем `min` в **единицах актива** (например `"5"` = 5 USDT), а в ответе `/list` он читается обратно как `min_minor` в **минимальных единицах** (например `"5000000"` для USDT с 6 знаками). Это одно и то же значение в разных единицах — не путайте. См. [Форматы сумм и денег](/reference/basics-money). * Суммы ниже порога остаются на балансе. * Механизм best‑effort и идемпотентный; адрес проходит комплаенс‑скрининг. *** ## Связанные страницы # POST /v1/balance Source: https://docs.oblodai.com/reference/balance Доступные (расходуемые) балансы мерчанта — по одной записи на валюту. **URL:** `https://api.oblodai.com/v1/balance` · **Аутентификация:** обязательна · **Тело:** пустой объект `{}` — параметров нет. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Пример запроса ```bash cURL theme={null} TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/balance' '{}' \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/balance \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d '{}' ``` ```python Python theme={null} call("/v1/balance", {}) ``` ```js Node.js theme={null} await call("/v1/balance", {}); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "balance": { "merchant": [ { "currency": "USDT", "balance": "1240.75" }, { "currency": "BTC", "balance": "0.01843000" } ] } } } ``` По записи на валюту: `currency` + доступный `balance` (строкой). *** ## Нюансы * Показываются только счета **`available`** — «замороженные» части (например, `payout_held`) не включаются. * **Незрелые (maturing) средства.** Депозит зачисляется на баланс сразу, но до набора нужных подтверждений остаётся под maturity‑холдом (reorg‑безопасность). Такие средства **видны в `balance`**, но ещё не выводимы: выплата на сумму, включающую незрелые средства, вернёт `409 payout.funds_maturing`. Поэтому выводимый остаток может быть временно **меньше** показанного. Разбивку available/maturing метод **не возвращает**; рабочая стратегия — повтор с backoff по `409`, см. [Устойчивый клиент](/guides/resilient-client#не-все-409-финальны) и [модель баланса](/guides/balance-and-funds). *** ## Связанные страницы общая картина available/maturing/held. списывает доступный баланс. # Аутентификация и подпись запросов Source: https://docs.oblodai.com/reference/basics-auth Каждый запрос к API (кроме публичных) авторизуется **тремя HTTP‑заголовками**. Тело запроса подписывается секретом вашего ключа по алгоритму **HMAC‑SHA256**. Подпись считается только на вашем сервере — секрет никогда не должен попадать в браузер или мобильное приложение. Если вы разбираетесь с подписью впервые, начните с инструкции [Как подписать запрос](/guides/signing-requests) — там тот же материал по шагам и с разбором ошибок. Эта страница — краткий справочник. *** ## Ключи мерчанта При онбординге вы получаете **один API‑ключ** — пару значений `public_id` + `secret`: | Значение | Назначение | | ----------- | ---------------------------------------------------------------------------------------------------------------- | | `public_id` | Несекретный идентификатор ключа (`oblodai_…`). Можно логировать. Уходит в заголовке `X-Public-Id`. | | `secret` | Секрет для подписи (`oblodai_live_…`). Показывается **один раз** — сохраните его. Никогда не передаётся по сети. | Этот один ключ аутентифицирует **весь** функционал — и приём платежей, и выплаты, и возвраты. Отдельные ключи заводить не нужно. *** ## Заголовки запроса | Заголовок | Значение | | ------------- | -------------------------------------------------------------------------------------------------- | | `X-Public-Id` | Ваш `public_id`. | | `X-Timestamp` | Текущее время в **unix‑секундах** (целое). Должно попадать в окно **±5 минут** от времени сервера. | | `X-Signature` | HMAC‑подпись запроса (hex, нижний регистр). | *** ## Как считается подпись Собирается «каноническая строка» из четырёх частей, склеенных символом перевода строки `\n`, и подписывается `secret`‑ом по HMAC‑SHA256: ``` signing_string = "{X-Timestamp}" + "\n" + "{METHOD}" + "\n" + "{path}" + "\n" + "{body}" X-Signature = hex( HMAC_SHA256( secret, signing_string ) ) ``` * `METHOD` — HTTP‑метод в верхнем регистре, то есть всегда `POST`. * `path` — путь запроса вместе со строкой запроса (query), если она есть. Для JSON‑эндпоинтов query отсутствует, поэтому `path` — это просто путь, например `/v1/payment`. * `body` — **точные байты** тела запроса, ровно как они уходят в сеть. Подписывайте ту же строку, что и отправляете: порядок полей и пробелы должны совпадать байт‑в‑байт. Подпись сверяется на сервере в постоянном времени. Временная метка проверяется на окно ±5 минут (`MaxClockSkew`) — это гасит переигрывание перехваченной подписи. **Самая частая ошибка:** подписать одну строку JSON, а отправить другую — например, библиотека переупорядочила поля или добавила пробелы. Подпись считается по **байтам** тела. Сериализуйте тело **один раз** в переменную и используйте её и для подписи, и для отправки. Подпись **запроса** и подпись **вебхука** — это два разных алгоритма. Не путайте их. Формат подписи вебхука описан на странице [Объект вебхука](/reference/webhook-object). *** ## Примеры ```bash cURL + openssl theme={null} SECRET='oblodai_live_…ваш_секрет' PUBLIC_ID='oblodai_…ваш_public_id' TS=$(date +%s) METHOD='POST' PATHQ='/v1/payment' BODY='{"order_id":"order-1001","currency":"USDT","network":"tron","amount":"25.00"}' SIGSTR=$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATHQ" "$BODY") SIG=$(printf '%s' "$SIGSTR" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com$PATHQ \ -X POST \ -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} import hmac, hashlib, time, json, requests SECRET = b"oblodai_live_…" PUBLIC_ID = "oblodai_…" BASE = "https://api.oblodai.com" def call(path: str, payload: dict) -> dict: body = json.dumps(payload, separators=(",", ":")) # компактный JSON — подписываем ровно его ts = str(int(time.time())) signing = f"{ts}\nPOST\n{path}\n{body}" sig = hmac.new(SECRET, signing.encode(), hashlib.sha256).hexdigest() r = requests.post(BASE + path, data=body, headers={ "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, }) return r.json() print(call("/v1/payment", {"order_id": "order-1001", "currency": "USDT", "network": "tron", "amount": "25.00"})) ``` ```js Node.js theme={null} import crypto from "node:crypto"; const SECRET = "oblodai_live_…"; const PUBLIC_ID = "oblodai_…"; const BASE = "https://api.oblodai.com"; async function call(path, payload) { const body = JSON.stringify(payload); // подписываем ровно эту строку const ts = Math.floor(Date.now() / 1000).toString(); const signing = `${ts}\nPOST\n${path}\n${body}`; const sig = crypto.createHmac("sha256", SECRET).update(signing).digest("hex"); const res = await fetch(BASE + path, { method: "POST", headers: { "Content-Type": "application/json", "X-Public-Id": PUBLIC_ID, "X-Timestamp": ts, "X-Signature": sig, }, body, }); return res.json(); } ``` ```php PHP theme={null} true, CURLOPT_POSTFIELDS => $body, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', "X-Public-Id: $publicId", "X-Timestamp: $ts", "X-Signature: $sig", ], ]); return json_decode(curl_exec($ch), true); } ``` *** ## Ошибки аутентификации | Код | HTTP | Причина | | ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `auth.bad_timestamp` | 401 | `X-Timestamp` **отсутствует или не число**. (Само окно ±5 минут проверяется позже — см. `merchant.bad_signature`.) | | `auth.ip_not_allowed` | 401 | IP‑адрес не входит в включённый [IP‑allowlist](/reference/api-allowlist). | | `merchant.bad_signature` | 401 | Подпись не совпала **или** `X-Timestamp` вне окна ±5 минут (проверка skew встроена в сверку подписи). | | `merchant.unknown_key` | 401 | Неизвестный или отозванный `X-Public-Id`. | | `merchant.wrong_key_kind` | **403** | Только для старых **раздельных** ключей: ключ не имеет права на этот эндпоинт. Единый API‑ключ (`oblodai_…`) подходит везде и эту ошибку не вызывает. | *** ## Связанные страницы пошаговая инструкция. почему подписи недостаточно для защиты от дублей. ограничение доступа по IP. другой алгоритм подписи, для входящих вебхуков. # Формат ответа и коды ошибок Source: https://docs.oblodai.com/reference/basics-errors Ответ на запрос содержит HTTP‑статус, стандартные заголовки и тело в формате JSON. HTTP‑статус отражает **класс** ошибки, а тело содержит **точный** машиночитаемый код. *** ## Тело ответа при успехе HTTP `200 OK` и конверт с `state: 0`: ```json theme={null} { "state": 0, "result": { "...": "полезные данные операции" } } ``` Подробнее — [Формат взаимодействия](/reference/basics-format). *** ## Тело ответа при ошибке Конверт с объектом `error`: ```json theme={null} { "error": { "code": "payout.insufficient_funds", "message": "insufficient available balance" } } ``` * **`code`** — машиночитаемый код вида `<домен>.<причина>`. Именно по нему ветвитесь в коде. * **`message`** — человекочитаемое пояснение. Логировать безопасно, но не полагайтесь на точный текст. **Одно исключение — ограничение частоты (`429`).** Оно отдаёт **другую** форму: `{"state":1,"message":"rate limit exceeded"}` — **без** объекта `error` и без `code`. Ваш обработчик ошибок должен учитывать оба варианта: сначала проверьте HTTP‑статус (`429` → подождать `Retry-After` и повторить), и только для прочих читайте `error.code`. Подробнее — [Ограничение частоты](/reference/basics-ratelimit). *** ## Классы ошибок (HTTP‑статусы) | Код | Когда возникает | Что делать | | -------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` `invalid` | Некорректный запрос: битый JSON, отсутствует поле, недопустимое значение. | Исправить запрос. Повтор без изменений не поможет. | | `401` `unauthorized` | Проблема аутентификации: неверная подпись, протухшая метка времени, неизвестный/отозванный ключ, IP не в allowlist. | Проверить подпись, часы, ключ, [allowlist](/reference/api-allowlist). | | `403` `forbidden` | Аутентификация прошла, но операция запрещена (заморозка, чужой ресурс и т. п.). | Разобраться в причине; повтор не поможет. | | `404` `not_found` | Объект не найден (или скрыт как не найденный, чтобы нельзя было пробить существование чужого UUID). | Проверить идентификатор. | | `409` `conflict` | Конфликт состояния: повтор с другим содержимым, объект в неподходящем статусе. | Зависит от кода: `payout.funds_maturing` / `idempotency.in_progress` / `payout.frozen` — повторить позже; прочие — проверить состояние через `*/info`. | | `429` rate limit | Превышена частота запросов. Тело `{"state":1,"message":…}` + `Retry-After: 60`. | Подождать `Retry-After` секунд и повторить. См. [лимит частоты](/reference/basics-ratelimit). | | `503` `unavailable` | Временная недоступность зависимости (например, оракул курсов). | Повторить с экспоненциальным backoff. | | `500` `internal` | Внутренняя ошибка. Детали не раскрываются. | Повторить с backoff. | *** ## Примеры кодов Список не исчерпывающий — точные коды каждого метода смотрите на его странице. | Код | Значение | | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `auth.bad_timestamp` | `X-Timestamp` отсутствует или не число (само окно ±5 минут — код `merchant.bad_signature`). | | `auth.ip_not_allowed` | IP вне включённого allowlist. | | `request.bad_json` | Тело не парсится как JSON. | | `payment.network_required` | У валюты несколько сетей, а `network` не задан. | | `payment.below_minimum` | Сумма ниже минимума сети. | | `payout.insufficient_funds` | Недостаточно доступного баланса. | | `payout.funds_maturing` | Средства ещё дозревают (maturity‑холд). | | `payout.duplicate_reference` | Гонка одновременных вставок с одним и тем же `reference`/`order_id`. Обычный повтор (ретрай) не ошибка — вернётся уже созданная выплата. | | `payout.amount_below_fee` | Сумма выплаты меньше сетевой комиссии. | *** ## Как обрабатывать ошибки 1. **Ветвитесь по `error.code`, а не по `message`.** Текст может меняться; код — контракт. 2. **`4xx` — ваша ошибка запроса.** Повтор без изменений даст тот же результат (кроме идемпотентных сценариев, где `409` означает «уже обработано»). 3. **`5xx` и `503` — временные.** Повторяйте с экспоненциальным backoff. Благодаря [идемпотентности](/reference/basics-idempotency) повтор денег‑движущих операций безопасен. 4. **`409` — не всегда финал.** `payout.funds_maturing`, `idempotency.in_progress`, `payout.frozen` — временные: повторяйте позже с backoff. Прочие `409` не трактуйте вслепую как «уже сделано» — сверьтесь через `*/info`. → [Устойчивый клиент](/guides/resilient-client#не-все-409-финальны) *** ## Связанные страницы все коды одной таблицей. # Формат взаимодействия Source: https://docs.oblodai.com/reference/basics-format Почти все методы API Oblodai — это HTTP‑запросы **`POST`** с телом в формате **JSON**. Так работают даже операции чтения: тело для них — либо пустой объект `{}`, либо объект с фильтрами. **`GET`‑эндпоинтов всего три**, и все они публичные (без подписи): | Метод | Назначение | | --------------------------------------------- | ------------------------------------------------------------- | | [`GET /v1/currencies`](/reference/currencies) | Каталог валют и сетей. | | [`GET /v1/link/{id}`](/reference/link-public) | Публичные данные [платёжной ссылки](/reference/payment-link). | | [`GET /v1/pay/{id}`](/reference/pay-get) | Публичные данные счёта для hosted‑страницы оплаты. | Всё остальное — `POST`. **Базовый URL:** `https://api.oblodai.com` *** ## Запрос | Что | Значение | | ------------------------ | -------------------------------------------------------------------- | | HTTP‑метод | `POST` (кроме трёх публичных `GET` выше) | | Заголовок `Content-Type` | `application/json` | | Тело | JSON‑объект (для чтения — `{}` или фильтры) | | Аутентификация | Три заголовка подписи — см. [Аутентификация](/reference/basics-auth) | | Ограничение размера тела | 1 МиБ | ### Заголовки запроса | Заголовок | Обяз. | Значение | | ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Content-Type` | да | `application/json` | | `X-Public-Id` | да\* | Несекретный идентификатор API‑ключа. | | `X-Timestamp` | да\* | Unix‑секунды на момент подписи. | | `X-Signature` | да\* | HMAC‑SHA256 канонической строки. | | `Idempotency-Key` | **нет** | Уникальное значение (≤255 символов) для безопасного повтора создающих операций: платежа, выплаты, возврата, кошелька, перевода, батча. Одинаковое во всех попытках одного действия. → [Идемпотентность](/reference/basics-idempotency) | \* Кроме публичных методов. ### Заголовки ответа | Заголовок | Когда приходит | Значение | | --------------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------- | | `Idempotent-Replayed: true` | На повторе с уже использованным `Idempotency-Key` | Ответ **воспроизведён**: новый объект не создан, вернулся ранее сохранённый результат. | | `Retry-After` | При `429` | Через сколько секунд повторять. → [Ограничение частоты](/reference/basics-ratelimit) | Пример «пустого» запроса на чтение (баланс): ```bash theme={null} curl -s https://api.oblodai.com/v1/balance \ -X POST \ -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" \ -d '{}' ``` *** ## Успешный ответ При успешной обработке (HTTP `200 OK`) API всегда возвращает **конверт** с полем `state: 0` и полезной нагрузкой в `result`: ```json theme={null} { "state": 0, "result": { "...": "полезные данные операции" } } ``` `result` может быть объектом, массивом или примитивом — зависит от метода. Конкретную форму смотрите на странице соответствующего эндпоинта. **Единственное исключение** — [`POST /v1/webhooks`](/reference/webhooks-register): он возвращает `201 Created` и «голый» объект без конверта `state`/`result`. Это оговорено на его странице. *** ## Ответ с ошибкой При ошибке возвращается конверт с объектом `error`, а HTTP‑статус отражает класс ошибки: ```json theme={null} { "error": { "code": "payout.insufficient_funds", "message": "insufficient available balance" } } ``` * `code` — машиночитаемый код вида `<домен>.<причина>`. **Ветвитесь в коде по нему.** * `message` — человекочитаемое пояснение. Безопасно логировать, но не полагайтесь на его точный текст. Полный разбор классов и кодов — [Формат ответа и коды ошибок](/reference/basics-errors). *** ## Связанные страницы # Идемпотентность Source: https://docs.oblodai.com/reference/basics-idempotency Повторный вызов создающего эндпоинта **не должен создавать второй объект**. Иначе обычный сетевой таймаут (ответ потерялся, хотя платёж уже создан) обернётся дублем — вторым счётом или второй выплатой. Защититься можно двумя способами — они работают вместе. ## 1. Заголовок `Idempotency-Key` (рекомендуемый) Как пользоваться: * **Значение** — любая уникальная строка **до 255 символов** (обычно UUID v4). Сгенерируйте его **до первой отправки** запроса и передавайте **одно и то же** во всех повторах одного действия. Новое действие — новый ключ. * **Где работает** — на всех создающих операциях: платёж, выплата, возврат, статический кошелёк, перевод, батч. * **Поведение повтора** — повтор с тем же ключом вернёт **тот же ответ**, что и первая успешная попытка (второй объект не создаётся), и заголовок ответа `Idempotent-Replayed: true` — по нему видно, что это переигранный результат, а не новая операция. ```bash theme={null} curl -X POST https://api.oblodai.com/v1/payment \ -H "Idempotency-Key: 3f2a9c6e-8d41-4b7a-9c05-1e6f2b8d4a70" \ ... ``` ## 2. Ваш `order_id` Второй, бизнес-уровень дедупликации — он работает, даже если заголовок забыли: * **Платежи** — `order_id` необязателен, но настоятельно рекомендуется. Повтор с `order_id`, для которого уже есть **живой** счёт, вернёт этот же счёт, а не создаст дубль (терминальный или просроченный счёт создание нового не блокирует). Проверка-и-создание защищены advisory-локом по паре `(мерчант, order_id)` от гонки одновременных запросов. * **Выплаты** — `order_id` **обязателен** (без него — `400 payout.order_id_required`). Идемпотентность по `(мерчант, order_id)`: повтор вернёт уже созданную выплату. Механизмы независимы и работают вместе: `order_id` дедуплицирует на уровне бизнес-сущности, а заголовок `Idempotency-Key` защищает сам HTTP-вызов. Заголовок поддерживают: `POST /v1/payment`, `/v1/payment/refund`, `/v1/payment/resolve`, `/v1/payment/batch`, `/v1/refund/batch`, `/v1/payout`, `/v1/payout/mass`, `/v1/payout/batch`, `/v1/transfer/to-personal`. На [выплатных ссылках](/reference/payout-link) заголовок не действует — там дедупликация через поле `reference`. ## Связанные страницы # Форматы сумм и денег Source: https://docs.oblodai.com/reference/basics-money Все денежные суммы в API передаются и возвращаются как **строки в десятичном виде** в единицах самой валюты. Никаких float, никаких «копеек» в основном формате запросов. *** ## Правила * **Суммы — строки.** Например `"25.00"` USDT, `"0.015"` BTC, `"3450.12000000"` для курса. Не число, а именно строка — это исключает потерю точности на 18‑значных токенах. * **В единицах валюты, а не в minor‑единицах.** `"25.00"` — это 25 USDT, а не 25 «копеек». * **Разделитель дробной части — точка.** Разделителя тысяч нет. * **Число знаков после точки зависит от валюты и сети.** Ориентируйтесь на [каталог сетей](/reference/basics-networks) и не обрезайте лишнее самостоятельно. Внутри шлюз считает деньги на целочисленной арифметике (big‑integer), без float. Вам тоже рекомендуется хранить суммы строками или в minor‑единицах и не пропускать их через `float`/`double`. *** ## Валюта цены и валюта расчёта У валюты в API две разные роли — не путайте их. | Поле | Роль | Что может быть | | ------------- | ------------------------------------ | ------------------------------------------------------------------------------- | | `currency` | **Валюта цены** — сколько счёт стоит | Любой из 23 фиатов (`USD`, `EUR`, `RUB`, …) или любая монета (`USDT`, `BTC`, …) | | `to_currency` | **Валюта расчёта** — чем платят | Только крипта; фиат невозможен | Полный список валют цены — `pricing_currencies` в [`GET /v1/currencies`](/reference/currencies); список валют расчёта — `currencies` там же. ### Цену можно назначать в фиате — не только в долларах Поддерживаются **23 фиатные валюты**: | | Валюты | Знаков после запятой | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | | Обычные | `USD`, `EUR`, `GBP`, `RUB`, `UAH`, `PLN`, `CZK`, `TRY`, `CNY`, `INR`, `BRL`, `CAD`, `AUD`, `CHF`, `AED`, `ZAR`, `MXN`, `IDR`, `THB`, `VND`, `NGN` | **2** | | Без дробной части | `JPY`, `KRW` | **0** | **У `JPY` и `KRW` ноль знаков после запятой.** У иены и воны нет разменной единицы: правильно `"amount": "10000"`, а `"10000.00"` — ошибка формата. ```python Python theme={null} call("/v1/payment", {"amount": "100", "currency": "EUR", "order_id": "o-1", "to_currency": "USDT", "network": "tron"}) # 100.00 EUR → USDT по курсу call("/v1/payment", {"amount": "5000", "currency": "RUB", "order_id": "o-2"}) # монету выберет покупатель call("/v1/payment", {"amount": "10000", "currency": "JPY", "order_id": "o-3", "to_currency": "USDT", "network": "tron"}) # без копеек! ``` ```js Node.js theme={null} // 100.00 EUR → USDT по курсу await call("/v1/payment", { amount: "100", currency: "EUR", order_id: "o-1", to_currency: "USDT", network: "tron" }); // монету выберет покупатель await call("/v1/payment", { amount: "5000", currency: "RUB", order_id: "o-2" }); // без копеек! await call("/v1/payment", { amount: "10000", currency: "JPY", order_id: "o-3", to_currency: "USDT", network: "tron" }); ``` **Тенге (`KZT`), сом (`KGS`) и сум (`UZS`) пока не поддерживаются** — источник курсов не котирует в них крипту напрямую. Счёт в такой валюте вернёт `400 payment.unknown_currency`. Цена в евро или рублях так же надёжна, как в долларах: курс монеты берётся **сразу в нужной валюте**, одной котировкой, без перемножения двух курсов и без второго провайдера. ### Расчёт всегда в крипте Шлюз не хранит фиат: он не держит долларовых или рублёвых счетов, не принимает и не отправляет фиат. Фиатная `currency` — это только «ценник»: сумма пересчитывается в крипту по курсу в момент фиксации, и дальше все деньги живут исключительно в монетах. Из этого следуют три правила: * **Баланс** мерчанта — в криптоактивах (`USDT`, `BTC`, …), не в фиате. См. [`/v1/balance`](/reference/balance). * **Выплаты** ([`/v1/payout`](/reference/payout-create)) отправляются в крипте на криптоадрес. * **Возвраты** ([`/v1/payment/refund`](/reference/payment-refund)) — в той монете, которая **фактически была получена**. Если счёт был выставлен в `EUR`, а покупатель заплатил `USDT`, вернётся `USDT` — ровно та монета и та сумма, что пришли. Возврат **не** пересчитывается заново по сегодняшнему курсу и не «выравнивается» до фиатной цены: курсовая разница между оплатой и возвратом не компенсируется. Счёт, у которого монета расчёта ещё не выбрана покупателем (валюто‑агностичный, `is_multi: true`), возвращать нечего — попытка вернёт `refund.nothing_to_refund`. **Из фиата монету расчёта вывести нельзя.** Если цена в фиате и задана `network`, но не задана `to_currency`, — `400 payment.to_currency_required`. Либо задайте `to_currency` явно, либо не задавайте и `network` тоже: тогда монету выберет покупатель. Правило одинаково для **любого** фиата, не только `USD`. → [`POST /v1/payment`](/reference/payment-create) *** ## Исключение: minor‑единицы Часть полей отдаётся именно в **минимальных единицах** (minor units) строкой — это всегда явно оговаривается на странице метода. Примеры: * [`POST /v1/referral/info`](/reference/referral-info) — `earnings_by_asset` в minor‑единицах (для USDT 6 знаков: `"18450000"` = 18.45 USDT). * [`POST /v1/auto-withdraw/*`](/reference/auto-withdraw) — порог `min_minor` в минимальных единицах. Если поле называется `*_minor` или на странице сказано «в минимальных единицах» — это minor‑формат, а не единицы валюты. *** ## Курсы Курс валюты возвращается строкой с фиксированной точностью, например `"3450.12000000"`. См. [`POST /v1/exchange-rate/list`](/reference/exchange-rate-list). Курс — оценочная рыночная котировка для отображения, а не гарантия исполнения: фактический курс платежа фиксируется в момент создания инвойса. *** ## Связанные страницы # Поддерживаемые сети и валюты Source: https://docs.oblodai.com/reference/basics-networks Метод оплаты или выплаты задаётся **парой коротких кодов**: `currency` (код валюты) + `network` (код сети). Ниже — полный список поддерживаемых пар на момент версии документа. **Уточняйте программно.** Публичный каталог всех активов и сетей — [`GET /v1/currencies`](/reference/currencies) (без ключа). Списки методов для приёма/выплат **вашего** мерчанта — [`POST /v1/payment/services`](/reference/payment-services) и [`POST /v1/payout/services`](/reference/payout-services). Каталог может меняться, а флаг `is_available` показывает, доступен ли метод прямо сейчас. *** ## Поддерживаемые пары | `currency` | `network` | Описание | | ---------- | ----------- | --------------------------------- | | `USDT` | `tron` | Tron USDT (TRC‑20) | | `USDT` | `ethereum` | Ethereum USDT (ERC‑20) | | `USDT` | `bsc` | BNB Smart Chain USDT (BEP‑20) | | `USDT` | `polygon` | Polygon USDT | | `USDT` | `avalanche` | Avalanche C‑Chain USDT | | `USDT` | `solana` | Solana USDT (SPL) | | `USDT` | `ton` | TON USDT (Jetton) | | `USDT` | `base` | Base USDT | | `USDT` | `arbitrum` | Arbitrum USDT | | `USDC` | `ethereum` | Ethereum USDC | | `USDC` | `bsc` | BNB Smart Chain USDC | | `USDC` | `polygon` | Polygon USDC | | `USDC` | `avalanche` | Avalanche C‑Chain USDC | | `USDC` | `solana` | Solana USDC (SPL) | | `USDC` | `base` | Base USDC | | `USDC` | `arbitrum` | Arbitrum USDC | | `DAI` | `ethereum` | Ethereum DAI | | `DAI` | `polygon` | Polygon DAI | | `POL` | `polygon` | Polygon — нативный POL | | `POL` | `ethereum` | Ethereum POL (ERC‑20) | | `ETH` | `ethereum` | Ethereum — нативный ETH | | `BNB` | `bsc` | BNB Smart Chain — нативный BNB | | `AVAX` | `avalanche` | Avalanche C‑Chain — нативный AVAX | | `TRX` | `tron` | Tron — нативный TRX | | `SOL` | `solana` | Solana — нативный SOL | | `TON` | `ton` | TON — нативный TON | | `BTC` | `bitcoin` | Bitcoin | *** ## Коды сетей `ethereum`, `bsc`, `polygon`, `avalanche`, `base`, `arbitrum`, `tron`, `solana`, `ton`, `bitcoin`. *** ## Важные отличия (частые вопросы) * **AVAX / `avalanche` — поддерживается** (нативный AVAX на C‑Chain). * **Нативный ETH — только на `ethereum`.** На L2 (`base`, `arbitrum`) поддерживаются только токены USDT/USDC. Нативного ETH там **нет**. * **Bitcoin‑семейство — только `BTC`.** `litecoin`, `dogecoin`, `bitcoincash`, `dash`, `monero` в API намеренно **не предлагаются**: создание инвойса или кошелька на такую сеть будет отклонено. Не используйте эти сети. *** ## Как задаётся сеть * Если у валюты **несколько** сетей, `network` обязателен. Иначе — `payment.network_required` при создании счёта или `payout` без сети. * Если у валюты **ровно одна** сеть, `network` можно не передавать — он подставится автоматически. * Для валюто‑агностичного счёта пустыми оставляют **`to_currency` и `network`** — монету и сеть выберет покупатель. См. [Три режима создания счёта](/guides/payment-modes). **`currency` обязателен ВСЕГДА.** Это валюта цены — без неё счёт не имеет стоимости. Пустым можно оставить только `to_currency` (валюту расчёта) и `network`. Цену задавайте в фиате (`USD`, `EUR`, `RUB`, …) или в монете — см. [Форматы сумм и денег](/reference/basics-money). *** ## Связанные страницы доступные методы приёма. доступные методы выплат. # Ограничение частоты и IP‑allowlist Source: https://docs.oblodai.com/reference/basics-ratelimit ## Ограничение частоты (rate limit) К API применяется ограничение частоты запросов. | Параметр | Значение | | -------------------- | ------------------------------------------------------------------------------------------------ | | **Бюджет** | по умолчанию **120 запросов в минуту** | | **На что считается** | на **IP‑адрес** клиента (не на API‑ключ и не на мерчанта) | | **Окно** | фиксированное окно в одну минуту (счётчик заводится на первом запросе и обнуляется через минуту) | | **При превышении** | `HTTP 429`, заголовок `Retry-After: 60`, тело `{"state":1,"message":"rate limit exceeded"}` | **Ключ — это IP.** Лимит общий для всех запросов с одного IP: все запросы вашего backend делят один бюджет, и разносить операции по разным API‑ключам бесполезно. Точное значение в проде может отличаться от 120/мин по умолчанию, поэтому **не «зашивайте» 120 в логику** — ориентируйтесь на ответ `429` и заголовок `Retry-After`. ### Что делать при 429 Обратите внимание: тело 429 — это `{"state":1,"message":…}`, **не** стандартный конверт `{"error":{…}}`. Правильная реакция: 1. Прочитайте `Retry-After` (секунды) и **подождите** это время, либо повторяйте с **экспоненциальным backoff**. 2. Повторите тот же запрос. Благодаря [идемпотентности](/reference/basics-idempotency) по `order_id` повтор денег‑движущих операций безопасен — дубля не возникнет. Все [SDK](/sdk/overview) делают это автоматически: ловят 429, читают `Retry-After` и повторяют. ### Как не упереться в лимит * **Не распараллеливайте сотнями.** Держите разумную конкурентность (единицы–десятки одновременных запросов), а не «выстрел» из сотни. * **Кэшируйте редко меняющееся.** Ответы [`/v1/payment/services`](/reference/payment-services), [`/v1/currencies`](/reference/currencies), [`/v1/exchange-rate/list`](/reference/exchange-rate-list) можно кэшировать на минуты — не запрашивайте их на каждый показ страницы. * **Не поллите статус.** Не опрашивайте [`/v1/payment/info`](/reference/payment-info) в цикле — статус вам сообщит [вебхук](/reference/webhook-object). Опрос уместен только как редкий фолбэк. * **Массовые операции — батчами.** Это **штатный способ не упираться в лимит**: батч из 5000 элементов — это **один** запрос в бюджете, а не 5000. Используйте [`/v1/payment/batch`](/reference/payment-batch), [`/v1/refund/batch`](/reference/refund-batch), [`/v1/payout/batch`](/reference/payout-batch) и опрашивайте результат через [`/v1/batch/info`](/reference/batch-info). Устаревший [`/v1/payout/mass`](/reference/payout-mass) (до 100, синхронный) годится только для маленьких пачек. *** ## IP‑allowlist Для вашего API‑ключа можно включить **список доверенных IP‑адресов**. Когда список включён, подписанные запросы с IP вне списка отклоняются с ошибкой `401 auth.ip_not_allowed`. По умолчанию allowlist выключен. В проде рекомендуется его включить и ограничить доступ IP‑адресами вашего backend. Управление списком — отдельные методы: | Метод | Назначение | | ----------------------------------------------------------- | ---------------------------- | | [`POST /v1/api-allowlist/list`](/reference/api-allowlist) | Показать список и статус. | | [`POST /v1/api-allowlist/add`](/reference/api-allowlist) | Добавить IP или CIDR. | | [`POST /v1/api-allowlist/remove`](/reference/api-allowlist) | Удалить запись. | | [`POST /v1/api-allowlist/enable`](/reference/api-allowlist) | Включить/выключить контроль. | Полное описание — на странице [IP‑allowlist](/reference/api-allowlist). *** ## Связанные страницы # POST /v1/batch/info Source: https://docs.oblodai.com/reference/batch-info Статус батча и результат по каждому его элементу. Это метод **поллинга**: батчи ([платежей](/reference/payment-batch), [возвратов](/reference/refund-batch), [выплат](/reference/payout-batch)) обрабатываются асинхронно, и `/v1/batch/info` — единственный способ узнать, что с ними стало. **URL:** `https://api.oblodai.com/v1/batch/info` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Идентификатор батча из ответа на submit. Сколько элементов вернуть в `items` (пагинация). Смещение по элементам. Счётчики `total` / `succeeded` / `failed` считаются по **всему** батчу и от пагинации не зависят — для отслеживания прогресса достаточно их, `items` можно не выбирать целиком. *** ## Пример запроса ```bash cURL theme={null} BODY='{"batch_id":"9f4c1a2b-77de-4a55-9c1f-0e2b3d4a5f60","limit":100,"offset":0}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/batch/info' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/batch/info \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/batch/info", { "batch_id": "9f4c1a2b-77de-4a55-9c1f-0e2b3d4a5f60", "limit": 100, "offset": 0, }) ``` ```js Node.js theme={null} await call("/v1/batch/info", { batch_id: "9f4c1a2b-77de-4a55-9c1f-0e2b3d4a5f60", limit: 100, offset: 0, }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "batch_id": "9f4c1a2b-77de-4a55-9c1f-0e2b3d4a5f60", "kind": "payment", "status": "completed", "on_error": "continue", "total": 1000, "succeeded": 998, "failed": 2, "created_at": "2026-07-13T12:00:00Z", "updated_at": "2026-07-13T12:01:14Z", "items": [ { "idx": 0, "status": "done", "order_id": "order-1", "result": { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "order_id": "order-1", "amount": "10.00", "payer_currency": "USDT", "address": "TJ4b1...C9xk", "url": "https://pay.oblodai.com/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e" } }, { "idx": 7, "status": "error", "order_id": "order-8", "error": "unsupported network for this currency" } ] } } ``` ### Поля ответа Идентификатор батча. Вид батча: `payment` | `refund` | `payout`. Статус батча: `pending` | `processing` | `completed`. Режим обработки ошибок, с которым батч был отправлен: `continue` | `stop`. Всего элементов в батче. Успешно обработано. Завершилось ошибкой. Время приёма батча (ISO 8601). Время последнего изменения (ISO 8601). Страница элементов (см. ниже). ### Элемент `items[]` Порядковый номер элемента в исходном массиве (с нуля). Статус элемента: `pending` | `processing` | `done` | `error`. `order_id` элемента, если вы его задавали. Присутствует не всегда. Результат успешной операции — **тот же объект**, что вернул бы одиночный вызов ([платежа](/reference/payment-object), возврата, [выплаты](/reference/payout-object)). Только при `status: "done"`. **Человекочитаемое сообщение** об ошибке (напр. `"unsupported network for this currency"`), а не код вида `payment.*`. Только при `status: "error"`. `result` и `error` — взаимоисключающие: у успешного элемента есть `result`, у неудачного — `error`. Отсутствующее поле просто не приходит. **`error` — это `message`, а не `code`.** В отличие от общего [конверта ошибки](/reference/basics-errors), у элемента батча машиночитаемого кода нет. Для логики ветвитесь по `status` (`done` / `error`), а текст используйте для логов и диагностики. *** ## Статусы **Батч:** `pending` (принят, воркер ещё не начал) → `processing` (элементы обрабатываются) → `completed` (обработка закончена). **`completed` означает «обработка закончена», а НЕ «всё успешно».** Батч с ошибками тоже приходит в `completed`. Смотрите на `succeeded` и `failed`. **Элемент:** `pending` → `processing` → `done` | `error`. **При `on_error: "stop"`** после первой ошибки остальные элементы батча обрабатываться не будут: они получают `status: "error"` с сообщением `"skipped: batch stopped after an earlier failure"` и **считаются в `failed`**. Отличить их от настоящих ошибок можно по этому тексту. *** ## Коды ошибок | Код | Значение | | ---------------------- | -------------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 batch.bad_id` | `batch_id` не является корректным UUID. | | `404 batch.not_found` | Батч не найден (или принадлежит другому мерчанту). | | `503 batch.disabled` | Массовая обработка недоступна на этом шлюзе. | | `401 auth.*` | Ошибки аутентификации. | *** ## Нюансы * **Поллите разумно.** Опрашивайте раз в несколько секунд с backoff, а не в тугом цикле: `/batch/info` тоже считается в [лимит частоты](/reference/basics-ratelimit). Смысла в опросе чаще, чем идёт обработка, нет. * **Про оплату счетов `/batch/info` ничего не знает.** Он показывает только, **создались** ли объекты. О том, что счёт оплачен, вам сообщит [вебхук](/reference/webhook-object). * **Частичный успех — норма.** При `on_error: "continue"` смотрите на `succeeded`/`failed` и разбирайте `items[].error`, а не «прошёл / не прошёл весь батч». *** ## Связанные страницы # Получение по выплатной ссылке Source: https://docs.oblodai.com/reference/claim-public Публичные эндпоинты страницы получения: посмотреть чек и забрать средства на свой адрес. **Методы:** `GET /v1/claim/{token}` · `POST /v1/claim/{token}` Эндпоинты страницы получения [выплатной ссылки](/reference/payout-link). **Публичные** — подпись и ключ не нужны: право доступа даёт сам одноразовый токен из `claim_url` (256 бит, хранится у нас только хешем). Эти запросы делает получатель средств (или ваша страница получения). ## GET /v1/claim/ Данные чека для страницы получения. Ничего мерчант-приватного в ответе нет. ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/claim/Xk3v… ``` ```json Пример ответа theme={null} { "state": 0, "result": { "status": "funded", "amount": "25", "currency": "USDT", "network": "tron", "title": "Бонус", "note": "Спасибо за участие", "expires_at": "2026-07-22T17:00:00Z", "claimable": true } } ``` `claimable` = `true`, пока ссылка в статусе `funded` и срок не вышел. ## POST /v1/claim/ Получатель указывает адрес — из резерва порождается обычная выплата. ### Параметры запроса Адрес получателя в сети чека. Destination tag / memo — для сетей, где он нужен (например, TON). ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/claim/Xk3v… \ -X POST -H 'Content-Type: application/json' \ -d '{"address":"TXYZ…"}' ``` ```js Node.js theme={null} const res = await fetch(`https://api.oblodai.com/v1/claim/${token}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ address: "TXYZ…" }), }).then(r => r.json()); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "status": "claimed", "payout_id": "a1b2c3d4-…", "amount": "25", "currency": "USDT", "network": "tron", "address": "TXYZ…" } } ``` **Claim идемпотентен по адресу.** Повторный POST с тем же адресом на уже полученном чеке — успех с тем же `payout_id`, вторая выплата не создаётся. Повтор с **другим** адресом отклоняется: чек навсегда привязан к адресу первого получения. ## Коды ошибок Пустой адрес. **Повтор:** Нет — передайте `address`. Некорректный адрес для сети. **Повтор:** Нет — проверьте адрес. Адрес не соответствует сети чека. **Повтор:** Нет. Адрес принадлежит шлюзу — получение на внутренние адреса запрещено. **Повтор:** Нет — укажите внешний кошелёк. Адрес не прошёл проверку. **Повтор:** Нет. Чек не найден — битый токен. **Повтор:** Нет. Срок чека вышел, резерв возвращён отправителю. **Повтор:** Нет. Чек отменён отправителем. **Повтор:** Нет. Получение уже идёт с другим адресом. **Повтор:** Только с тем же адресом, что был первым. ## Связанные страницы # GET /v1/currencies Source: https://docs.oblodai.com/reference/currencies Публичный каталог принимаемых активов и сетей, на которых их можно оплачивать/выводить — данные для страницы «Наши монеты» или для построения списка выбора валюты в чекауте. Отдаёт **два списка**: `currencies` — в чём можно **получать** (валюты расчёта, только крипта), и `pricing_currencies` — в чём можно **назначать цену** (**38 записей: 15 монет + 23 фиата**). Разница между ролями — [Форматы сумм и денег](/reference/basics-money). **URL:** `https://api.oblodai.com/v1/currencies` · **Метод:** `GET` · **Аутентификация:** не требуется (публичные данные). *** ## Параметры запроса Нет. Обычный `GET` без тела и без подписи. *** ## Пример запроса ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/currencies ``` ```python Python theme={null} import requests r = requests.get("https://api.oblodai.com/v1/currencies") print(r.json()) ``` ```js Node.js theme={null} const r = await fetch("https://api.oblodai.com/v1/currencies"); console.log(await r.json()); ``` *** ## Пример ответа Ответ отдаётся **напрямую** (без конверта `{state, result}`, как у публичных `GET`-эндпоинтов): ```json theme={null} { "currencies": [ { "symbol": "USDT", "decimals": 6, "networks": [ { "network": "tron", "kind": "token", "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "min_confirmations": 20, "available": true, "deposit_available": true, "payout_available": true } ] } ], "pricing_currencies": [ { "symbol": "USD", "decimals": 2, "fiat": true }, { "symbol": "EUR", "decimals": 2, "fiat": true }, { "symbol": "RUB", "decimals": 2, "fiat": true }, { "symbol": "JPY", "decimals": 0, "fiat": true }, { "symbol": "USDT", "decimals": 6, "fiat": false }, { "symbol": "BTC", "decimals": 8, "fiat": false } ] } ``` ### `currencies` — в чём можно получать Валюты **расчёта**: чем покупатель платит и в чём мерчант получает деньги. Только крипта. Список активов. Код актива (`USDT`, `BTC`, …). Число знаков после запятой у актива. Сети, на которых доступен актив. Код сети (`tron`, `ethereum`, …). `native` (монета сети) или `token`. Адрес контракта токена (только для `token`; у `native` отсутствует). Полный reorg‑безопасный порог подтверждений для этой сети. Доступен ли **приём** (депозиты) — синоним `deposit_available`. Доступен ли приём платежей в этой сети прямо сейчас. Доступны ли выплаты в этой сети прямо сейчас. ### `pricing_currencies` — в чём можно назначать цену Валюты **цены**: допустимые значения поля `currency` в [`POST /v1/payment`](/reference/payment-create). Это **15 монет плюс 23 фиата** — всего 38 записей. Набор монет здесь шире списка `currencies`: цену можно назначить и в монете, которой нет среди валют расчёта. Список валют ценообразования (38 записей). Код валюты цены (`USD`, `EUR`, `RUB`, `USDT`, `BTC`, …). Число знаков после точки. У большинства фиатов — 2, у `JPY` и `KRW` — **0**. `true` — фиат: в нём можно **только назначать цену**, но не получать оплату. Сетей у элемента `pricing_currencies` нет: валюта цены не определяет, как пойдут деньги, — это задаёт валюта расчёта `to_currency`. #### Поддерживаемые фиатные валюты (23) | Знаков | Валюты | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **2** | `USD`, `EUR`, `GBP`, `RUB`, `UAH`, `PLN`, `CZK`, `TRY`, `CNY`, `INR`, `BRL`, `CAD`, `AUD`, `CHF`, `AED`, `ZAR`, `MXN`, `IDR`, `THB`, `VND`, `NGN` | | **0** | `JPY`, `KRW` — у иены и воны нет разменной единицы: `"10000"`, а не `"10000.00"` | **Тенге (`KZT`), сом (`KGS`), сум (`UZS`) не поддерживаются** — счёт в такой валюте вернёт `payment.unknown_currency`. *** ## Нюансы * **Цена и расчёт — разные списки.** Элемент с `fiat: true` (`USD`, `EUR`, `RUB`, …) годится только для поля `currency`; передать его в `to_currency` нельзя — расчёт всегда идёт в крипте. Если цена в фиате и задана `network`, но не задана `to_currency`, создание счёта вернёт `payment.to_currency_required`. * **Не хардкодьте список фиатов.** Берите `pricing_currencies` из этого метода: набор валют может расширяться. * **Приём и выплаты разделены.** `deposit_available` и `payout_available` независимы: сеть может быть доступна только на приём или только на вывод. Стройте UI по нужному направлению. * **Актуальный источник.** Список отражает текущую конфигурацию шлюза — используйте его, а не захардкоженный перечень, чтобы новые сети появлялись автоматически. Сводный перечень пар «на бумаге» — в [Поддерживаемые сети и валюты](/reference/basics-networks). * Для методов приёма/выплат **конкретного мерчанта** (с лимитами и комиссией) — см. [`POST /v1/payment/services`](/reference/payment-services) и [`POST /v1/payout/services`](/reference/payout-services). *** ## Связанные страницы # Диаграммы: жизненные циклы и потоки Source: https://docs.oblodai.com/reference/diagrams Визуальные схемы ключевых процессов Oblodai. Диаграммы написаны на Mermaid — они рендерятся в большинстве просмотрщиков Markdown (включая GitHub). Если ваш просмотрщик не поддерживает Mermaid, рядом с каждой есть текстовое описание. *** ## Поток приёма платежа (sequence) Кто с кем взаимодействует при обычном платеже. ```mermaid theme={null} sequenceDiagram participant B as Покупатель participant S as Ваш backend participant O as Oblodai participant Chain as Блокчейн S->>O: POST /v1/payment (создать счёт) O-->>S: uuid, address, payer_amount, url S-->>B: показать адрес / ссылку на оплату B->>Chain: отправить средства на address Chain-->>O: транзакция замечена O->>O: набор подтверждений (reorg-безопасность) O-->>S: webhook invoice.paid (после порога) S->>S: проверить подпись, дедуп, выдать товар S-->>O: 200 OK ``` **Текстом:** ваш backend создаёт счёт → показывает покупателю адрес → покупатель платит в сеть → Oblodai видит транзакцию и ждёт подтверждений → присылает вебхук `invoice.paid` → вы проверяете подпись, дедуплицируете и выдаёте товар, отвечая `2xx`. *** ## Жизненный цикл платежа (статусы) Как счёт переходит между [статусами `payment_status`](/reference/payment-object#статусы-payment_status). ```mermaid theme={null} stateDiagram-v2 [*] --> check: счёт создан [*] --> select: валюто-агностичный счёт создан select --> check: покупатель выбрал монету и сеть select --> cancel: истёк до выбора check --> confirm_check: транзакция замечена check --> cancel: истёк / отменён confirm_check --> paid: оплачено в допуске confirm_check --> paid_over: переплата confirm_check --> wrong_amount_waiting: недоплата, срок не вышел wrong_amount_waiting --> paid: доплатили в срок wrong_amount_waiting --> wrong_amount: срок вышел paid --> [*] paid_over --> [*] wrong_amount --> [*] cancel --> [*] ``` **Текстом:** `check` (ждём оплату) → `confirm_check` (ждём подтверждений) → далее `paid`, `paid_over` или `wrong_amount_waiting`. Недоплата в срок может дойти до `paid`, иначе — `wrong_amount`. Неоплаченный счёт истекает в `cancel`. Валюто-агностичный счёт (`is_multi: true`) начинается в `select` и после выбора монеты и сети покупателем переходит в `check`; истёкший без выбора уходит в `cancel`. Терминальные (`is_final`): `paid`, `paid_over`, `wrong_amount`, `cancel`. *** ## Жизненный цикл выплаты (статусы) Внутренний цикл и его отображение в укрупнённый [`status`](/reference/payout-object#статусы-выплаты). ```mermaid theme={null} stateDiagram-v2 [*] --> pending: создана (status: check) pending --> awaiting_cosign: нужен второй подписант awaiting_cosign --> approved: второй co-sign получен pending --> approved: одобрена approved --> broadcasting: отправляется broadcasting --> sent: отправлена (status: process) sent --> confirmed: подтверждена (status: paid) pending --> cancelled: отменена (status: cancel) approved --> failed: сбой (status: fail) sent --> failed: сбой отправки (status: fail) confirmed --> [*] failed --> [*] cancelled --> [*] ``` `awaiting_cosign` — для мерчантов с обязательным co‑sign: выплата ждёт второй подписи, прежде чем перейти в `approved`. В укрупнённом `status` это отображается как `check`. **Текстом:** внутренний путь `pending → approved → broadcasting → sent → confirmed` (для мерчантов с обязательным co‑sign между `pending` и `approved` вклинивается `awaiting_cosign` — ожидание второй подписи). В ответах API это сворачивается в `check` (pending/awaiting\_cosign), `process` (approved/broadcasting/sent), `paid` (confirmed), `fail` (failed), `cancel` (cancelled). По API‑ключу выплата авто‑одобряется и сразу идёт в `process`. Внутренние состояния (`pending`/`awaiting_cosign`/`approved`/`broadcasting`/`sent`/`confirmed`) показаны для справки; в API вам приходит укрупнённый `status` — см. [объект выплаты](/reference/payout-object). *** ## Обработка вебхука (flow) Что должен делать ваш обработчик на каждый входящий вебхук. ```mermaid theme={null} flowchart TD A["Пришёл вебхук"] --> B{"is_test?"} B -->|"да"| Z["Ответить 200"] B -->|"нет"| C["Взять сырое тело + заголовки"] C --> D{"Подпись верна?"} D -->|"нет"| E["Ответить 403"] D -->|"да"| F{"uuid + status уже виден?"} F -->|"да"| Z F -->|"нет"| G["Обработать событие"] G --> H{"Обработка успешна?"} H -->|"да"| I["Запомнить ключ, ответить 200"] H -->|"нет"| J["Ответить 5xx — придёт ретрай"] ``` **Текстом:** пробное тело (`is_test`) — сразу `200`. Иначе проверить подпись по сырому телу (неверна — `403`), затем дедуп по `uuid` + `status` (уже видели — `200`, no‑op), затем обработать. Успех — `200` и запомнить ключ; неуспех — не‑2xx, чтобы Oblodai повторил доставку. → [Настройка вебхуков](/guides/webhooks-setup) *** ## Три режима создания счёта (выбор) Какой режим получится в зависимости от переданных полей. `currency` (валюта цены) обязателен всегда; пустыми можно оставить `to_currency` и `network`. ```mermaid theme={null} flowchart TD A["POST /v1/payment
currency обязателен"] --> B{"to_currency и network заданы?"} B -->|"оба заданы"| C["Одновалютный счёт
адрес сразу"] B -->|"to_currency задан, network пуст"| D{"у монеты одна сеть?"} D -->|"да"| E["Авто-сеть
адрес сразу"] D -->|"нет"| F["Ошибка
payment.network_required"] B -->|"to_currency пуст, network задан"| H{"currency — фиат или монета?"} H -->|"монета
USDT, BTC, …"| I["to_currency = currency
адрес сразу"] H -->|"фиат
USD, EUR, RUB, …"| J["Ошибка
payment.to_currency_required"] B -->|"оба пусты"| G["Валюто-агностичный счёт
is_multi true, адрес после выбора"] ``` **Текстом:** заданы валюта расчёта и сеть → обычный счёт. Задана только валюта расчёта → сеть подставится, если она единственная, иначе `payment.network_required`. Задана только сеть: если цена в **монете** — валютой расчёта станет она же; если цена в **фиате** (`USD`, `EUR`, `RUB`, …) — вывести из неё монету расчёта невозможно, будет `payment.to_currency_required`. Ни `to_currency`, ни `network` не заданы → покупатель выбирает монету и сеть на странице. → [Три режима создания счёта](/guides/payment-modes) *** ## Инвойс vs статический кошелёк (выбор) Что использовать под задачу. ```mermaid theme={null} flowchart TD A["Нужно принять оплату"] --> B{"Фиксированная сумма
за конкретный заказ?"} B -->|"да"| C["Инвойс
POST /v1/payment"] B -->|"нет, пополнение баланса"| D["Статический кошелёк
POST /v1/wallet"] C --> E["Вебхук invoice.*"] D --> F["Вебхук wallet.paid"] ``` **Текстом:** разовая покупка на фиксированную сумму — инвойс. Постоянный адрес пополнения (депозиты, баланс клиента) — статический кошелёк. → [Статические кошельки](/guides/static-wallets) *** ## Связанные страницы # Справочник кодов ошибок Source: https://docs.oblodai.com/reference/errors-catalog Полный список кодов ошибок API Oblodai, сгруппированных по домену. Для каждого — HTTP‑статус, значение и что делать. Это сводная страница для отладки: увидели код → нашли строку. Общие правила обработки, HTTP‑классы и формат конверта ошибки — на странице [Формат ответа и коды ошибок](/reference/basics-errors). Пошаговая диагностика по симптомам («что делать если…») — [Что делать, если не работает](/guides/troubleshooting). **Ветвитесь по `error.code`, а не по `message`.** Текст сообщения может меняться; код — контракт. *** ## Как читать каталог * Серый бейдж у кода — **HTTP-статус** ответа (отражает класс: `4xx` — ваша ошибка запроса, `5xx`/`503` — временная). * **Повтор:** — это и есть краткое «что делать»: «нет — \<как исправить>» значит сначала поправьте запрос; «да (backoff)» — временная ошибка, повторяйте с задержкой (повтор денег‑движущих операций безопасен благодаря [идемпотентности](/reference/basics-idempotency) — заголовку `Idempotency-Key` и/или `order_id`). Если по коду непонятно, что делать, — найдите симптом в [хабе диагностики](/guides/troubleshooting). *** ## auth — аутентификация `X-Timestamp` отсутствует или не число. **Повтор:** Нет — передайте unix‑секунды. IP вне включённого [IP‑allowlist](/reference/api-allowlist). **Повтор:** Нет — добавьте IP в allowlist. Подпись не совпала или `X-Timestamp` вне окна ±5 минут. **Повтор:** Нет — проверьте подпись; синхронизируйте часы (NTP). Неизвестный или отозванный `X-Public-Id`. **Повтор:** Нет — проверьте ключ. Только для старых **раздельных** ключей: ключ без права на этот эндпоинт. Единый API‑ключ (`oblodai_…`) подходит везде. **Повтор:** Нет — используйте единый API‑ключ. Сервер не смог прочитать тело запроса (обрыв соединения при отправке). **Повтор:** Да — повторите запрос. Диагностика подписи — [Как подписать запрос](/guides/signing-requests). *** ## request — общий разбор запроса Тело не парсится как JSON. **Повтор:** Нет — исправьте тело. *** ## idempotency — заголовок `Idempotency-Key` Приходят на создающих методах, если вы прислали заголовок [`Idempotency-Key`](/reference/basics-idempotency). Запрос с этим ключом **ещё выполняется**. Объект, возможно, уже создаётся. **Повтор:** Да (backoff) — повторите тот же запрос с тем же ключом чуть позже; получите исходный ответ. Некорректный `Idempotency-Key` (пустой или длиннее 255 символов). **Повтор:** Нет — исправьте ключ. Хранилище идемпотентности временно недоступно. Запрос **не выполнен** — молча продублировать операцию шлюз не станет. **Повтор:** Да (backoff) — повторите с тем же ключом. *** ## payment — приём платежей Неизвестная `currency`. **Повтор:** Нет. Неизвестная `to_currency`. **Повтор:** Нет. Цена в фиате (`currency: "USD"`), задана `network`, но не задана `to_currency`: из долларов монету расчёта вывести нельзя. **Повтор:** Нет — задайте `to_currency` (монету, в которой платят) либо уберите и `network` тоже — тогда монету выберет покупатель. У валюты несколько сетей, `network` не задан. **Повтор:** Нет — укажите сеть. Пара валюта+сеть не поддерживается. **Повтор:** Нет — см. [каталог](/reference/basics-networks). Некорректная сумма. **Повтор:** Нет. `subtract` вне 0–100. **Повтор:** Нет. `accuracy_payment_percent` вне 0–5. **Повтор:** Нет. Сумма ниже минимума сети. **Повтор:** Нет — увеличьте сумму или смените сеть. `url_callback` не валидный http(s)-URL (или приватный адрес). **Повтор:** Нет — исправьте URL. Некорректный формат `uuid`. **Повтор:** Нет. Не передан ни `uuid`, ни `order_id`. **Повтор:** Нет. Счёт не найден (или скрыт как чужой). **Повтор:** Нет — проверьте идентификатор. Методы: [`/v1/payment`](/reference/payment-create), [`/v1/payment/info`](/reference/payment-info). *** ## invoice — движок счёта Ошибки самого движка счёта. Всплывают при создании ([`/v1/payment`](/reference/payment-create)), выборе валюты на hosted‑странице ([`/v1/pay/{id}/select`](/reference/pay-select)) и при переоценке счёта. Некорректная сумма счёта. **Повтор:** Нет — исправьте сумму. Не удалось определить актив оплаты. **Повтор:** Нет — задайте `to_currency`/`network`. Счёт не в статусе `select` (валюта уже выбрана). **Повтор:** Нет. Срок счёта истёк. **Повтор:** Нет — создайте новый счёт. Состояние счёта изменилось параллельно (reorg/гонка). **Повтор:** Да — перечитайте счёт и повторите. Оракул курса временно недоступен. **Повтор:** Да — повторите с backoff. Не удалось оценить по курсу фиксированную часть комиссии (\$0.30 с платежа). Счёт **не создаётся** — комиссия не «обнуляется» молча. **Повтор:** Да — повторите с backoff. Не удалось выделить депозит‑адрес. **Повтор:** Да — повторите с backoff. *** ## pay — hosted‑страница оплаты Некорректный `{id}` в пути. **Повтор:** Нет. Счёт не найден. **Повтор:** Нет. Валюта уже выбрана (счёт не в статусе `select`). **Повтор:** Нет. Неизвестная валюта при выборе. **Повтор:** Нет. Метод не входит в набор [`accepted`](/reference/payment-accepted). **Повтор:** Нет. Сумма ниже минимума сети. **Повтор:** Нет. Методы: [`GET /v1/pay/{id}`](/reference/pay-get), [`POST /v1/pay/{id}/select`](/reference/pay-select). *** ## wallet — статические кошельки Неизвестная валюта. **Повтор:** Нет. Не указана сеть. **Повтор:** Нет. Пара валюта+сеть не поддерживается. **Повтор:** Нет. Не передан `address`. **Повтор:** Нет. Некорректный `uuid` кошелька. **Повтор:** Нет. Статический кошелёк не найден. **Повтор:** Нет — проверьте `uuid`. Методы: [`/v1/wallet`](/reference/wallet-create), [`/v1/wallet/block`](/reference/wallet-block), [`/v1/wallet/blocked-address-refund`](/reference/wallet-blocked-refund). *** ## qr — генерация QR Не передан `address`. **Повтор:** Нет. Метод: [`/v1/wallet/qr`](/reference/wallet-qr). *** ## payout — выплаты Неизвестная валюта. **Повтор:** Нет. Некорректная сумма. **Повтор:** Нет. Не передан `order_id`. **Повтор:** Нет — задавайте всегда. Адрес принадлежит шлюзу (self‑dealing). **Повтор:** Нет — укажите внешний адрес. Не передан адрес назначения. **Повтор:** Нет. Пустой/некорректный адрес. **Повтор:** Нет. Адрес не соответствует выбранной сети. **Повтор:** Нет — проверьте сеть. Валюта и сеть не соответствуют друг другу. **Повтор:** Нет. Выплаты заморожены (kill‑switch). **Повтор:** Да (backoff) — после снятия заморозки. Неподдерживаемая конвертация (только `USDT`→`currency`). **Повтор:** Нет. `from_currency` совпадает с валютой выплаты — конвертировать нечего. **Повтор:** Нет. Некорректная сумма для конвертации из `from_currency`. **Повтор:** Нет. Такая конвертация не поддерживается. **Повтор:** Нет. Нет курса для конвертации (оракул недоступен). **Повтор:** Да — повторите с backoff. Конвертации временно заморожены. **Повтор:** Да (backoff) — после снятия. Недостаточно баланса в `from_currency` для конвертации. **Повтор:** Да, после пополнения. `memo` длиннее 120 символов. **Повтор:** Нет. Некорректный `url_callback` (SSRF). **Повтор:** Нет. Сумма меньше сетевой комиссии. **Повтор:** Нет — увеличьте сумму. Некорректный `uuid`. **Повтор:** Нет. Не передан ни `uuid`, ни `order_id`. **Повтор:** Нет. Выплата не найдена (или скрыта как чужая). **Повтор:** Нет — проверьте `uuid`/`order_id`. Пустой массив `payouts` (mass). **Повтор:** Нет. Больше 100 элементов (mass). **Повтор:** Нет — режьте на пачки. Выплата не в статусе `pending` (approve). **Повтор:** Нет. Подтверждающий совпадает с создателем (maker‑checker). **Повтор:** Нет. Недостаточно доступного баланса. **Повтор:** Да, после пополнения. Средства ещё дозревают (maturity‑холд). **Повтор:** Да (backoff) — дождитесь подтверждений. Гонка двух одновременных вставок с одним `reference`/`order_id`. Обычный повтор — не ошибка (вернётся уже созданная выплата). **Повтор:** Повторите — идемпотентность вернёт существующую выплату; либо сверьтесь через [`/payout/info`](/reference/payout-info). Методы: [`/v1/payout`](/reference/payout-create), [`/v1/payout/mass`](/reference/payout-mass), [`/v1/payout/info`](/reference/payout-info), [`/v1/payout/approve`](/reference/payout-approve). *** ## payoutlink — выплатные ссылки (крипто-чеки) Неизвестный крипто-актив. **Повтор:** Нет. Некорректная сумма. **Повтор:** Нет. Недостаточно доступного баланса для резерва. **Повтор:** Да, после пополнения. Средства ещё дозревают. **Повтор:** Да, позже с backoff. Пустой массив `links`. **Повтор:** Нет. Больше 500 элементов в батче ссылок. **Повтор:** Нет — разбейте на страницы. Некорректный `link_id`. **Повтор:** Нет. Ссылка не найдена (или чужая), для claim — битый токен. **Повтор:** Нет. Отменить можно только невостребованную ссылку. **Повтор:** Нет. Claim без адреса. **Повтор:** Нет — передайте `address`. Claim на внутренний адрес шлюза запрещён. **Повтор:** Нет. Срок ссылки вышел, резерв возвращён. **Повтор:** Нет. Ссылка отменена мерчантом. **Повтор:** Нет. Получение уже идёт с другим адресом. **Повтор:** Только с первым адресом. Функция выключена на шлюзе. **Повтор:** Нет. ## resolution — резолв недоплаты `action` не `accept`/`refund`. **Повтор:** Нет. Платёж не в статусе `wrong_amount`. **Повтор:** Нет — сверьтесь с `/v1/payment/info`. Решение по счёту уже принято. **Повтор:** Нет. По счёту уже был возврат — accept невозможен. **Повтор:** Нет. Функция выключена на шлюзе. **Повтор:** Нет. ## refund — возвраты Не передан адрес назначения. **Повтор:** Нет. Некорректная сумма. **Повтор:** Нет. Нечего возвращать. Также приходит на валюто‑агностичный счёт (`is_multi: true`), где покупатель ещё не выбрал монету: валюты расчёта нет — возвращать нечего. **Повтор:** Нет. Адрес назначения принадлежит шлюзу. **Повтор:** Нет. Сумма слишком мала (dust). **Повтор:** Нет. Больше оплаченной суммы вернуть нельзя. **Повтор:** Нет. Возврат переплаты превышает саму переплату. **Повтор:** Нет. Тот же `(платёж, адрес, сумма)` уже в работе. **Повтор:** Нет — это идемпотентность. Методы: [`/v1/payment/refund`](/reference/payment-refund), [`/v1/wallet/blocked-address-refund`](/reference/wallet-blocked-refund). *** ## batch — массовые операции Ошибки **уровня батча** (весь запрос отклонён). Ошибки отдельных элементов приходят не сюда, а в `items[].error` ответа [`/v1/batch/info`](/reference/batch-info) — там это коды соответствующего домена (`payment.*`, `refund.*`, `payout.*`). Массив элементов (`payments` / `refunds` / `payouts`) пуст. **Повтор:** Нет — добавьте элементы. Больше **5000** элементов в одном батче. **Повтор:** Нет — разбейте на несколько батчей. `batch_id` не является корректным UUID. **Повтор:** Нет — проверьте идентификатор. Батч не найден (или принадлежит другому мерчанту). **Повтор:** Нет. Массовая обработка недоступна на этом шлюзе. **Повтор:** Да (backoff) — либо отправляйте операции по одной. Методы: [`/v1/payment/batch`](/reference/payment-batch), [`/v1/refund/batch`](/reference/refund-batch), [`/v1/payout/batch`](/reference/payout-batch), [`/v1/batch/info`](/reference/batch-info). *** ## paylink — платёжные ссылки Часть кодов приходит вам (при создании ссылки), часть — покупателю (при оплате по ссылке). `link_id` / `{id}` не является корректным UUID. **Повтор:** Нет. Ссылка не найдена, выключена или истекла. **Повтор:** Нет — проверьте `link_id` или включите ссылку через `/toggle`. `amount_mode` не `fixed` / `open` / `range`. **Повтор:** Нет — исправьте режим. Неизвестная валюта ссылки. **Повтор:** Нет — см. `pricing_currencies` в [`/v1/currencies`](/reference/currencies). `amount_fixed` не положительное число (режим `fixed`). **Повтор:** Нет. `amount_min` не положительное число. **Повтор:** Нет. `amount_max` не положительное число. **Повтор:** Нет. `amount_min` больше `amount_max`. **Повтор:** Нет. Режим `open`/`range`, а покупатель не ввёл сумму. **Повтор:** Нет — покажите покупателю поле ввода суммы. Введённая сумма не положительная. **Повтор:** Нет. Введённая сумма меньше `amount_min`. **Повтор:** Нет — покажите покупателю минимум. Введённая сумма больше `amount_max`. **Повтор:** Нет — покажите покупателю максимум. Платёжные ссылки недоступны на этом шлюзе. **Повтор:** Да (backoff). Методы: [`/v1/payment/link` и др.](/reference/payment-link), [`GET /v1/link/{id}` · `/checkout`](/reference/link-public). При оплате по ссылке может прийти **любая ошибка создания счёта** (`payment.*`, `invoice.*`) — checkout проходит тот же путь, что и [`POST /v1/payment`](/reference/payment-create). *** ## split — сплит‑платежи Получатель не задан либо заданы **оба** (`address` и `merchant_id`). **Повтор:** Нет — задайте ровно один вариант. Задан `address`, но не задана `network`. **Повтор:** Нет — укажите сеть. `percent` вне диапазона 0–100. **Повтор:** Нет. Сумма долей всех правил превысила 100 %. **Повтор:** Нет — уменьшите доли или удалите правило. `merchant_id` не является корректным UUID. **Повтор:** Нет. Получатель сплита — вы сами. **Повтор:** Нет — укажите другого получателя. `refund_hold_hours` вне диапазона 0–2160 (90 суток). **Повтор:** Нет. `rule_id` не является корректным UUID. **Повтор:** Нет. Правило не найдено. **Повтор:** Нет. Сплит‑платежи недоступны на этом шлюзе. **Повтор:** Да (backoff). Методы: [`/v1/split/rule*`](/reference/split-rule), [`/v1/split/config/*`](/reference/split-config). *** ## email — письма покупателю Получатель не определён: не передан `email` и у платежа нет `payer_email`. **Повтор:** Нет — передайте `email` либо задавайте `payer_email` при создании счёта. Почта не настроена на этом шлюзе. **Повтор:** Да (backoff) — либо шлите письмо сами. Метод: [`/v1/payment/send-email`](/reference/payment-send-email). *** ## compliance — AML‑проверка Если у шлюза включён комплаенс‑провайдер, адрес назначения выплаты/возврата проверяется перед отправкой. Адрес назначения заблокирован AML‑проверкой. **Повтор:** Нет — используйте другой адрес. Для проверки не передан адрес назначения. **Повтор:** Нет. Методы: [`/v1/payout`](/reference/payout-create), [`/v1/payment/refund`](/reference/payment-refund), [`/v1/wallet/blocked-address-refund`](/reference/wallet-blocked-refund). *** ## transfer — перевод на личный кошелёк У мерчанта нет привязанного владельца. **Повтор:** Нет. Неизвестная валюта. **Повтор:** Нет. Некорректная сумма. **Повтор:** Нет. Сумма перевода некорректна (≤0 или неверный формат). **Повтор:** Нет. Недостаточно средств на балансе для перевода. **Повтор:** Да, после пополнения. Метод: [`/v1/transfer/to-personal`](/reference/transfer-to-personal). *** ## Настройки приёма ### accepted — принимаемые валюты Неизвестная валюта в наборе. **Повтор:** Нет — исправьте валюту. Не указана сеть для элемента. **Повтор:** Нет — укажите сеть. ### discount — скидки/наценки Неизвестная валюта. **Повтор:** Нет — исправьте валюту. `discount_percent` вне −99…99. **Повтор:** Нет — задайте значение в диапазоне. ### accuracy — допуск недоплаты При `enabled: true` значение `accuracy_percent` вне 1–5 %. **Повтор:** Нет — задайте значение 1–5 %. ### vrcs Ошибка чтения состояния VRCS. **Повтор:** Да (backoff) — повторите позже. *** ## Вебхуки, автовывод, allowlist ### webhook Не передан `url` / `url_callback`. **Повтор:** Нет — передайте URL. Некорректный или запрещённый (приватный/локальный) URL. **Повтор:** Нет — исправьте URL. Ваш эндпоинт не принял пробное тело. **Повтор:** Да (backoff) — после починки эндпоинта. ### autowithdraw Неизвестный актив. **Повтор:** Нет — исправьте актив. Некорректный порог. **Повтор:** Нет — исправьте порог. Не хватает обязательного поля. **Повтор:** Нет — добавьте поле. При `set` адрес также проходит валидацию сети — возможны `payout.bad_address` и `payout.address_network_mismatch` (см. раздел payout). ### apiallow — IP‑allowlist Некорректный IP или CIDR. **Повтор:** Нет — исправьте IP/CIDR. Нельзя включить контроль с пустым списком. **Повтор:** Нет — добавьте хотя бы один адрес. *** ## Общие серверные ошибки | HTTP | Класс | Значение | | ---- | ------------- | -------------------------------------------------------------------------------------------------------- | | 429 | rate limit | Превышен лимит частоты. В ответе — `Retry-After: 60`. См. [лимиты частоты](/reference/basics-ratelimit). | | 503 | `unavailable` | Временная недоступность зависимости (напр. оракул курсов). | | 500 | `internal` | Внутренняя ошибка, детали не раскрываются. | Все три ошибки транзиентны — повторяйте с backoff; для 429 дождитесь `Retry-After`. **Транзиентные коды с HTTP 503.** Кроме перечисленных выше, при недоступности зависимостей могут прийти доменные коды класса 503 — например `rates.no_source` / `rates.non_positive` / `rates.deviation` (курс), `payout.freeze_unknown` (хранилище kill‑switch), `ledger.unavailable` (учёт). Все они — **временные**: повторяйте тот же идемпотентный запрос с backoff. *** ## Связанные страницы HTTP‑классы и правила обработки. что делать при частых ошибках. термины, встречающиеся в описаниях. # POST /v1/exchange-rate/list Source: https://docs.oblodai.com/reference/exchange-rate-list Возвращает текущие обменные курсы к **USDT**. **Публичный метод.** Подпись и ключ не нужны — это витринные котировки, а не финансовая операция. Отличие от Heleket: здесь это `POST /v1/exchange-rate/list` (не `GET /v1/exchange-rate/{currency}/list`), и котировка даётся **к USDT**, а не к USD. **URL:** `https://api.oblodai.com/v1/exchange-rate/list` *** ## Параметры запроса Тело опционально. Код валюты. Если задан — вернётся курс только по нему. Если пусто или тело `{}` — по всем валютам. *** ## Пример запроса По всем валютам — тело `{}`. По одной валюте: ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/exchange-rate/list \ -X POST \ -H 'Content-Type: application/json' \ -d '{"currency_from":"ETH"}' ``` ```python Python theme={null} import requests r = requests.post("https://api.oblodai.com/v1/exchange-rate/list", json={"currency_from": "ETH"}) print(r.json()) ``` ```js Node.js theme={null} const res = await fetch("https://api.oblodai.com/v1/exchange-rate/list", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currency_from: "ETH" }), }); console.log(await res.json()); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": [ { "from": "ETH", "to": "USDT", "course": "3450.12000000" } ] } ``` Валюта, для которой дана котировка. Всегда `USDT`. Курс строкой с фиксированной точностью. *** ## Нюансы * **Неизвестная `currency_from`** вернёт пустой `result: []`, а не ошибку. * **Пары без котировки** просто опускаются из ответа. * Курс — **оценочная** рыночная котировка для отображения, не гарантия исполнения. Фактический курс платежа фиксируется при создании инвойса (см. [`POST /v1/payment`](/reference/payment-create)). *** ## Связанные страницы там курс фиксируется под конкретный счёт. # Глоссарий Source: https://docs.oblodai.com/reference/glossary Термины, которые встречаются в документации Oblodai. Если по ходу чтения попался незнакомый термин — он, скорее всего, здесь. *** ## Формат и протокол **Конверт (envelope)** — «обёртка» вокруг любого ответа API: поле `state` (успех/ошибка), а полезные данные лежат внутри `result`. → [Формат взаимодействия](/reference/basics-format) **`state`** — признак успеха ответа в конверте: `0` — всё хорошо (смотрите `result`); `1` (или наличие поля `error`) — произошла ошибка. **unix‑секунды / unix‑время** — способ записи момента времени числом: количество секунд, прошедших с 1 января 1970 года по UTC. Так задаётся, например, заголовок `X-Timestamp` и поле `expired_at`. **Heleket** — другой платёжный API. «Heleket‑совместимый» означает, что формат запросов и ответов у Oblodai совпадает с ним — чтобы миграцию можно было сделать почти без переписывания кода. **Best‑effort («по возможности»)** — операция выполняется, если получится, но её неуспех не ломает основной процесс. Например, побочное уведомление могут отправить best‑effort: не дошло — платёж всё равно засчитан. *** ## Ключи и подпись **`public_id`** — несекретный идентификатор вашего API‑ключа. Можно логировать. Уходит в заголовке `X-Public-Id`. → [Аутентификация](/reference/basics-auth) **`secret`** — секретная часть ключа, которой считается подпись запросов. Показывается один раз, хранится только на сервере. Не путать с секретом вебхука. **Секрет вебхука** — отдельный секрет, который возвращает [`/v1/webhooks`](/reference/webhooks-register); им проверяется подпись **входящих** вебхуков. Это другое значение, чем `secret` API‑ключа. **HMAC‑SHA256** — алгоритм, которым считается подпись: хеш от строки на основе секрета. Используется и для подписи запросов, и для подписи вебхуков — но по **разным** правилам сборки строки. **Каноническая строка** — строго определённая строка, которую подписывают. Для запроса это `timestamp\nMETHOD\npath\nbody`; для вебхука — `timestamp.сырое_тело`. → [Как подписать запрос](/guides/signing-requests) **Идемпотентность** — свойство операции, при котором повторный вызов не создаёт второй объект, а возвращает существующий. Делает ретраи безопасными. В Oblodai работают **два механизма сразу**: HTTP‑заголовок `Idempotency-Key` и ваш `order_id`. → [Идемпотентность](/reference/basics-idempotency) **`Idempotency-Key`** — HTTP‑заголовок с любым уникальным значением (≤255 символов), который вы шлёте на создающем запросе. Повтор **с тем же значением** вернёт тот же ответ (и заголовок `Idempotent-Replayed: true`), а не создаст второй объект. Ключ генерируется **до** первой отправки и одинаков во всех попытках. Если запрос с этим ключом ещё выполняется — `409 idempotency.in_progress`. **`Idempotent-Replayed`** — заголовок **ответа**: `true` означает, что ответ воспроизведён по `Idempotency-Key`, новый объект не создавался. *** ## Деньги **Валюта цены** (`currency`) — валюта, в которой вы назначаете **стоимость** счёта. Это любой из **23 фиатов** (`USD`, `EUR`, `GBP`, `RUB`, `UAH`, `JPY`, …) **или** любая монета. У `JPY` и `KRW` ноль знаков после запятой. → [Форматы сумм и денег](/reference/basics-money) **Валюта расчёта** (`to_currency`) — монета, которой покупатель **фактически платит** и в которой вы получаете деньги. **Только крипта, фиат невозможен**: шлюз не хранит фиат. Если цена в фиате и задана `network`, но не задана `to_currency`, — ошибка `payment.to_currency_required`. **`pricing_currencies`** — список валют цены (38 записей: 15 монет + 23 фиата) в ответе [`GET /v1/currencies`](/reference/currencies). Не путайте со списком `currencies` — тем, в чём можно **получать** оплату (только крипта). **Единицы валюты** — сумма в «человеческом» виде: `"25.00"` USDT = 25 USDT. Основной формат сумм в API. → [Форматы сумм и денег](/reference/basics-money) **Minor‑единицы (минимальные единицы)** — сумма в наименьших неделимых долях валюты, строкой. Для USDT (6 знаков) `"18450000"` = 18.45 USDT. Используется в отдельных полях (`earnings_by_asset`, `min_minor`) — всегда оговаривается. **Комиссия платформы (наша комиссия)** — то, что берёт Oblodai. По умолчанию 1.5 % + \$0.30 с каждого платежа (у приведённого реферала — 1.4 %). Удерживается и с инвойсов, и с депозитов на статический кошелёк (со статических — только процент, без фиксированных \$0.30). **Сетевая комиссия (газ)** — плата сети блокчейна за транзакцию. Кто её несёт при выплате — настраивается [`fee-config`](/reference/payout-fee-config). **Dust / пыле‑порог** — сумма настолько мелкая, что не покрывает даже сетевую комиссию за перевод. Такие возвраты отклоняются кодом `refund.dust`, а выплаты — кодом `payout.amount_below_fee`: отправить их физически невозможно. **Курс** — обменная котировка. У платежа фиксируется при создании и действует до `rate_expires_at`. Публичные витринные котировки — [`/v1/exchange-rate/list`](/reference/exchange-rate-list). **Оракул курсов** — сервис, который отдаёт актуальный курс криптовалюты. Именно от него берётся котировка, когда сумма в вашей валюте пересчитывается в крипту. **`subtract`** — устаревшее поле: процент сетевой наценки, перекладываемой на плательщика. Новичку не нужно — payer‑facing наценки настраиваются через [`discount`](/reference/payment-discount). *** ## Приём платежей **Инвойс (счёт)** — разовый платёжный объект на фиксированную сумму с ограниченным сроком. Создаётся [`/v1/payment`](/reference/payment-create). Оплата закрывает счёт. → [Объект платежа](/reference/payment-object) **Статический кошелёк** — постоянный адрес приёма без фиксированной суммы и срока; поступления падают на баланс. Создаётся [`/v1/wallet`](/reference/wallet-create). → [Объект кошелька](/reference/wallet-object) **Депозит‑адрес** — адрес в блокчейне, на который покупатель отправляет оплату по счёту (поле `address`). Именно его показывает hosted‑страница и QR‑код. **Hosted‑страница оплаты** — страница Oblodai, куда вы отправляете покупателя по ссылке `url`. Показывает адрес, QR, сумму, таймер и статус; работает без секрета мерчанта. **Валюто‑агностичный (deferred) счёт** — счёт, у которого валюту и сеть выбирает покупатель на hosted‑странице (`is_multi: true`). До выбора адрес и сумма пусты. → [Три режима](/guides/payment-modes) **`order_id`** — ваш идентификатор заказа/операции. Служит ключом идемпотентности и связывает объект Oblodai с вашим заказом. **`uuid`** — идентификатор объекта (платежа, выплаты, кошелька) на стороне Oblodai. **Допуск (accuracy)** — на сколько процентов недоплата всё ещё считается успешной оплатой. → [`/v1/payment/accuracy`](/reference/payment-accuracy) **Недоплата / переплата** — оплата меньше/больше ожидаемой суммы. Обрабатывается допуском и автовозвратом. → [Недоплата и переплата](/guides/under-overpayment) **Автовозврат (autorefund)** — автоматический возврат излишка при переплате или суммы при истёкшей недоплате. → [`/v1/payment/autorefund`](/reference/payment-autorefund) **`payer_address`** — адрес, **с которого пришли деньги**. Это адрес возврата по умолчанию: возврат без явного `address` уходит туда и авто‑подтверждается. Известен на аккаунтных сетях (EVM, Tron, Solana, TON); на Bitcoin/UTXO пуст — там единого отправителя нет. → [Объект платежа](/reference/payment-object) **`refund_status`** — сводка по возвратам счёта: `none` (ничего не возвращали), `partial` (вернули часть), `full` (вернули всё оплаченное). Приходит вместе с `refunds[]` в [`/v1/payment/info`](/reference/payment-info). **Платёжная ссылка** — многоразовый URL оплаты: каждый, кто по ней платит, получает свой отдельный счёт. **Единственный способ принимать платежи без своего бэкенда** (Tilda, Wix, письмо): счёт создаёт публичный `POST /v1/link/{id}/checkout`, подпись не нужна. → [Платёжные ссылки](/reference/payment-link) **Донат (открытая сумма, `amount_mode: "open"`)** — режим платёжной ссылки, где сумму вводит сам покупатель («заплати сколько хочешь»); `amount_min` — необязательный нижний порог. Соседние режимы: `fixed` (сумму задали вы) и `range` (покупатель выбирает в диапазоне «от 5 до 1000»). **Чек (receipt)** — письмо об успешной оплате. Уходит **автоматически** на `payer_email`, если тот задан у платежа. Не путать с письмом «Оплатите» — его вы шлёте сами через [`/v1/payment/send-email`](/reference/payment-send-email). *** ## Массовые операции и сплиты **Батч (batch)** — до **5000** операций (платежей, возвратов или выплат) в **одном** подписанном запросе. Обрабатывается асинхронно. Это **штатный способ не упираться в [лимит частоты](/reference/basics-ratelimit)**: батч стоит один запрос, сколько бы элементов в нём ни было. → [`/v1/payment/batch`](/reference/payment-batch) **`batch_id`** — идентификатор батча, который возвращается сразу при отправке. По нему статус и результат каждого элемента забираются через [`/v1/batch/info`](/reference/batch-info). Статусы батча: `pending` → `processing` → `completed` (последнее означает «обработка закончена», а не «всё успешно»). **`on_error`** — поведение батча при ошибке элемента: `continue` (по умолчанию — обрабатывать остальные) или `stop` (прекратить; оставшиеся элементы помечаются ошибкой). **Сплит (split)** — правило, по которому доля каждого входящего платежа автоматически уходит партнёру (например 10 % с каждого платежа). Для партнёрских программ и маркетплейсов. Сплит — **пятый путь** ухода средств с баланса. → [Сплит‑платежи](/reference/split-rule) **Refund‑hold (`refund_hold_hours`)** — окно удержания: на сколько часов откладывается отправка долей партнёрам. Смысл в том, что **возврат списывает с вас всю сумму, которую заплатил покупатель**, — если доли разослать сразу, на возврат денег не останется. Пока окно не истекло, возврат отменяет или уменьшает долю партнёра. Сплиты на внешний адрес после отправки **необратимы**; сплиты внутри платформы (`merchant_id`) отзываются обратно. → [`/v1/split/config`](/reference/split-config) *** ## Выплаты **Выплата (payout)** — вывод средств с баланса на внешний адрес. → [Объект выплаты](/reference/payout-object) **Авто‑одобрение** — выплаты по API‑ключу подтверждаются автоматически (`approval_required: false`) и уходят сразу, без ручного шага. **maker‑checker** — правило «создатель ≠ подтверждающий»: подтвердить выплату должен не тот, кто её создал. Применяется к кабинетным сценариям; для API‑ключа не нужно. → [`/v1/payout/approve`](/reference/payout-approve) **Автовывод (auto‑withdraw)** — правило автоматически выводить чистую сумму депозита на заданный адрес. → [`/v1/auto-withdraw/*`](/reference/auto-withdraw) **Личный кошелёк** — кошелёк владельца аккаунта, общий для всех его магазинов. По API доступен только ввод средств в него; вывод — в кабинете под 2FA. → [`/v1/transfer/to-personal`](/reference/transfer-to-personal) **Self‑dealing (выплата себе)** — попытка вывести средства на адрес, который принадлежит самому шлюзу Oblodai. Такая операция запрещена и отклоняется кодом `payout.destination_internal` (для возврата — `refund.destination_internal`). **Kill‑switch (аварийная заморозка)** — защитный механизм, который временно останавливает выплаты при подозрительной активности. Пока он активен, выплаты возвращают код `payout.frozen`. **Co‑sign / 2‑of‑2 / `awaiting_cosign`** — режим, где выплату должны подтвердить две стороны (нужна вторая подпись). До второго подтверждения выплата стоит во внутреннем статусе `awaiting_cosign`. Актуально только для мерчантов, у которых co‑sign включён обязательным; при обычном API‑ключе не встречается. *** ## Статусы и жизненный цикл **`is_final`** — признак терминального статуса: объект больше не изменится. **Терминальный статус** — конечное состояние объекта (для платежа: `paid`, `paid_over`, `wrong_amount`, истёкший/отменённый; для выплаты: `paid`, `fail`, `cancel`). **Укрупнённый статус выплаты** — Heleket‑совместимый статус в поле `status` (`check`/`process`/`paid`/ `fail`/`cancel`), в который сворачивается детальный внутренний жизненный цикл. → [Объект выплаты](/reference/payout-object#статусы-выплаты) **Broadcasting** — фаза, когда выплата уже отправляется в блокчейн и средства покидают горячий кошелёк. Это один из внутренних статусов выплаты; наружу он сворачивается в укрупнённый `process`. *** ## Блокчейн и безопасность **Подтверждения (confirmations)** — число блоков, подтвердивших транзакцию. Порог зачёта зависит от суммы и сети (`required_confirmations`). **Reorg (реорганизация сети)** — ситуация, когда сеть «переписывает» недавние блоки. Чтобы платёж не откатился, средства выдерживаются до безопасной глубины. **`txid`** — идентификатор (хеш) транзакции в блокчейне. По нему можно найти платёж или выплату в обозревателе сети. **L2 / сеть второго уровня** — надстройка над базовой сетью (например `base`, `arbitrum` над Ethereum) с гораздо более дешёвыми комиссиями. Для приёма средств это такая же сеть, как и другие. **Незрелые (maturing) средства** — депозит, уже зачисленный на баланс, но ещё не набравший подтверждений и потому невыводимый. Выплата на такую сумму вернёт `409 payout.funds_maturing`. → [`/v1/balance`](/reference/balance) **Maturity‑холд** — временная задержка вывода незрелых средств ради reorg‑безопасности. **SSRF‑проверка** — защита при регистрации URL вебхука: приватные и локальные адреса запрещены, чтобы нельзя было заставить шлюз обращаться во внутреннюю сеть. **IP‑allowlist** — список доверенных IP/CIDR; при включении запросы с других адресов отклоняются (`auth.ip_not_allowed`). → [IP‑allowlist](/reference/api-allowlist) **CIDR** — компактная запись диапазона IP‑адресов, например `203.0.113.0/24` (весь блок адресов `203.0.113.*`). Удобно, когда нужно разрешить сразу подсеть, а не один адрес. **Комплаенс‑скрининг** — проверка адреса назначения на санкционные/рисковые списки перед выплатой или возвратом. **VRCS (Volatility Risk Control System)** — авто‑конвертация волатильных депозитов в USDT для защиты от колебаний курса. → [`/v1/vrcs`](/reference/vrcs) *** ## Вебхуки **Вебхук (webhook)** — HTTP‑уведомление, которое Oblodai шлёт на ваш URL при событии (оплата, пополнение, статус выплаты). → [Объект вебхука](/reference/webhook-object) **at‑least‑once (как минимум один раз)** — гарантия доставки: вебхук доставляется хотя бы раз, но может прийти и несколько раз. Отсюда требование дедупликации. **Дедупликация** — отбрасывание повторно пришедшего вебхука по паре `uuid` + `status`, чтобы не обработать событие дважды. **transactional‑outbox** — приём, при котором запись о доставке вебхука пишется в той же транзакции, что и зачисление платежа, — уведомление не теряется. **dead‑letter (`dead`)** — статус доставки, исчерпавшей все попытки. Виден в [журнале доставок](/reference/webhooks-deliveries). **Backoff (экспоненциальный)** — стратегия повторов с растущей задержкой (10 с → удвоение → потолок 1 час). Применяется и к ретраям вебхуков, и рекомендуется вам при `5xx`/`503`. *** ## Связанные страницы # Публичная страница ссылки Source: https://docs.oblodai.com/reference/link-public **Методы:** `GET /v1/link/{id}` · `POST /v1/link/{id}/checkout` Публичные методы [платёжной ссылки](/reference/payment-link): показать ссылку покупателю и создать по ней счёт. **Публичные методы.** Аутентификация **не требуется** — их вызывает браузер покупателя, у которого нет и не должно быть вашего секрета. Именно поэтому платёжная ссылка — **единственный способ принимать платежи без бэкенда**: страницу на Tilda, Wix или в Notion можно подключить к оплате, ничего не подписывая. **Базовый URL:** `https://api.oblodai.com` *** ## GET /v1/link/\{id} Публичная карточка ссылки: что показывать покупателю и какой виджет суммы рисовать. ### Параметры пути | Параметр | Описание | | -------- | ----------------------------------------------------------------------- | | `{id}` | `link_id` из ответа [`POST /v1/payment/link`](/reference/payment-link). | ### Пример запроса ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40 ``` ```python Python theme={null} import requests r = requests.get("https://api.oblodai.com/v1/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40") print(r.json()) ``` ```js Node.js theme={null} const r = await fetch("https://api.oblodai.com/v1/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40"); console.log(await r.json()); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "link_id": "5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40", "title": "Поддержать проект", "description": "Спасибо!", "amount_mode": "open", "currency": "USD", "amount_min": "1.00" } } ``` | Поле | Тип | Описание | | ------------------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------- | | `link_id` | string | Идентификатор ссылки. | | `title` / `description` | string | Что показать покупателю. | | `amount_mode` | string | `fixed` — сумму не спрашивать; `open` — поле ввода суммы (**донат**); `range` — поле ввода с границами. | | `currency` | string | Валюта цены — в ней покупатель вводит сумму. | | `amount_fixed` | string | Сумма при `fixed`. | | `amount_min` / `amount_max` | string | Границы. Присутствуют, только если заданы. | | `pinned_currency` / `pinned_network` | string | Если есть — монета и сеть уже закреплены, выбор покупателю не показывать. Если полей нет — рисуйте выбор монеты и сети. | **Ответ намеренно не содержит ничего о мерчанте** — ни идентификаторов, ни настроек, ни статистики. Его безопасно запрашивать из браузера. ### Коды ошибок | Код | Значение | | ----------------------- | --------------------------------------------- | | `400 paylink.bad_id` | `{id}` не является корректным UUID. | | `404 paylink.not_found` | Ссылка не найдена, **выключена** или истекла. | | `503 paylink.disabled` | Платёжные ссылки недоступны на этом шлюзе. | *** ## POST /v1/link/\{id}/checkout Создать по ссылке **настоящий счёт** для этого покупателя. Возвращает объект платежа — с `uuid`, адресом и ссылкой `url` на [hosted‑страницу оплаты](/reference/pay-get), куда покупателя и нужно отправить. ### Параметры запроса Сумма, которую ввёл покупатель, в валюте цены ссылки. **Обязательна для `open` и `range`**; для `fixed` **игнорируется** (сумму задаёт ссылка). **Валюта расчёта** — монета, которой платит покупатель. Нужна, только если ссылка не закрепила `pinned_currency`. Сеть расчёта. Нужна, только если ссылка не закрепила `pinned_network`. Email покупателя. На него автоматически уйдёт **чек** после оплаты. Для `fixed` сумму можно не передавать вовсе. Если ссылка закрепила монету/сеть, переданные значения игнорируются — выигрывает закрепление. Если не закрепила и покупатель ничего не выбрал, счёт создастся [валюто‑агностичным](/guides/payment-modes): монету и сеть покупатель выберет уже на странице оплаты. ### Пример запроса ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40/checkout \ -X POST -H 'Content-Type: application/json' \ -d '{"amount":"10.00","currency":"USDT","network":"tron","payer_email":"buyer@example.com"}' ``` ```python Python theme={null} import requests r = requests.post( "https://api.oblodai.com/v1/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40/checkout", json={"amount": "10.00", "currency": "USDT", "network": "tron", "payer_email": "buyer@example.com"}, ) print(r.json()["result"]["url"]) # сюда отправляем покупателя ``` ```js Node.js theme={null} const r = await fetch( "https://api.oblodai.com/v1/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: "10.00", currency: "USDT", network: "tron", payer_email: "buyer@example.com" }), }, ); const { result } = await r.json(); window.location = result.url; // редирект на страницу оплаты ``` ### Пример ответа Обычный [объект платежа](/reference/payment-object) — точно такой же, как у [`POST /v1/payment`](/reference/payment-create): ```json theme={null} { "state": 0, "result": { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "amount": "10.00", "currency": "USD", "payer_amount": "10.150000", "payer_currency": "USDT", "network": "tron", "address": "TJ4b1...C9xk", "address_qr_code": "data:image/png;base64,iVBORw0KGgo...", "payment_status": "check", "is_multi": false, "url": "https://pay.oblodai.com/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "expired_at": 1783728000 } } ``` Дальше — обычный платёж: покупатель платит, вы получаете [вебхук](/reference/webhook-object). ### Коды ошибок | Код | Значение | | ------------------------------ | -------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 paylink.bad_id` | `{id}` не является корректным UUID. | | `404 paylink.not_found` | Ссылка не найдена, выключена или истекла. | | `400 paylink.amount_required` | Режим `open`/`range`, а `amount` не передан. | | `400 paylink.not_positive` | Сумма не положительная. | | `400 paylink.below_min` | Сумма меньше `amount_min`. | | `400 paylink.above_max` | Сумма больше `amount_max`. | | `400 paylink.bad_mode` | У ссылки некорректный режим суммы. | | `400 paylink.unknown_currency` | Неизвестная валюта. | | `503 paylink.disabled` | Платёжные ссылки недоступны на этом шлюзе. | Кроме них может прийти **любая ошибка создания счёта** — checkout выполняет тот же путь, что и [`POST /v1/payment`](/reference/payment-create): `payment.below_minimum`, `payment.network_required`, `payment.unsupported_network`, `invoice.quote_failed` и т. д. Показывайте покупателю понятный текст и давайте повторить. *** ## Нюансы * **Секрет в браузер не попадает.** Оба метода публичные — этим ссылка и отличается от прямого создания счёта. * **Каждый checkout — новый счёт.** Двое покупателей по одной ссылке получат разные `uuid` и разные адреса. Один и тот же покупатель, нажав дважды, тоже получит два счёта — идемпотентности здесь нет (её нечем ключевать: `order_id` покупатель не задаёт). * **Выключенная ссылка** отдаёт `404` на обоих методах, но уже созданные по ней счета остаются оплачиваемыми. * **`payer_email` = чек.** Если покупатель оставил email, после оплаты ему автоматически уйдёт чек. Прислать письмо со счётом («оплатите») можно и вручную — [`POST /v1/payment/send-email`](/reference/payment-send-email). *** ## Связанные страницы управление ссылками. # Справочник API Oblodai Source: https://docs.oblodai.com/reference/overview Точное описание каждого метода и объекта API Oblodai. В справочнике описаны все методы API Oblodai и все объекты, которыми он оперирует. Для каждого метода указаны параметры, их типы и обязательность, примеры запроса (cURL / Python / Node.js), пример ответа, коды ошибок и важные нюансы поведения. **API‑версия:** `v1` · **Базовый URL:** `https://api.oblodai.com` Почти все методы — `POST` с телом в формате JSON. `GET`‑эндпоинтов **три**, и все публичные: [`GET /v1/currencies`](/reference/currencies), [`GET /v1/link/{id}`](/reference/link-public) и [`GET /v1/pay/{id}`](/reference/pay-get). Все запросы, кроме явно помеченных «публичный», подписываются вашим API‑ключом. См. [Аутентификация и подпись запросов](/reference/basics-auth). **Про `call(...)` в примерах.** В примерах на Python/Node встречается хелпер `call("/v1/...", {...})` — это ваша функция‑обёртка, которая добавляет подпись запроса. Её готовое определение (на 4 языках) — в [Как подписать запрос](/guides/signing-requests). Проще не писать её вручную, а взять [SDK](/sdk/overview) — он подписывает за вас. Если вам нужна не отдельная справка по методу, а пошаговое руководство под задачу — откройте [Инструкции](/guides/overview). ## Разделы справочника Формат, подпись, ошибки, деньги, сети, лимиты. Объект платежа и все методы приёма — от создания счёта до автовозврата. Платёжные ссылки и распределение платежей между партнёрами. Батчи: до 5000 платежей, возвратов или выплат одним запросом. Статические кошельки, баланс мерчанта, рефералы. Объект выплаты, создание, подтверждение, комиссии, возвраты. Формат вебхука, регистрация, журнал доставок, тестовые события. Каталог валют и курсы — без API‑ключа. Каталог ошибок, глоссарий, диаграммы потоков. *** ## Как проходит платёж (за 20 секунд) Покупатель создаёт счёт и получает депозит‑адрес. Он платит в крипте на этот адрес, а сеть блокчейна подтверждает транзакцию. После достаточного числа подтверждений Oblodai присылает вам вебхук, и вы отмечаете заказ оплаченным. Наглядно — [диаграммы](/reference/diagrams). *** ## Основы работы с API * [Формат взаимодействия](/reference/basics-format) — `POST` + JSON, конверт `state`/`result`. * [Аутентификация и подпись запросов](/reference/basics-auth) — HMAC‑SHA256, три заголовка, примеры на 4 языках. * [Формат ответа и коды ошибок](/reference/basics-errors) — HTTP‑классы и коды вида `<домен>.<причина>`. * [Идемпотентность](/reference/basics-idempotency) — безопасные повторы: заголовок `Idempotency-Key` и `order_id`. * [Форматы сумм и денег](/reference/basics-money) — суммы строками, без float. * [Поддерживаемые сети и валюты](/reference/basics-networks) — пары `currency` + `network`. * [Ограничение частоты и IP‑allowlist](/reference/basics-ratelimit) — rate limit, доверенные IP. ## Справочные материалы * [Справочник кодов ошибок](/reference/errors-catalog) — все коды одной таблицей, сгруппированы по домену. * [Глоссарий](/reference/glossary) — все термины документации. * [Диаграммы: жизненные циклы и потоки](/reference/diagrams) — схемы платежей, выплат, вебхуков. *** ## Публичные методы (без ключа) * [`GET /v1/currencies`](/reference/currencies) — каталог: **два списка** — в чём можно получать (`currencies`) и в чём можно назначать цену (`pricing_currencies`: 23 фиата + монеты). * [`POST /v1/exchange-rate/list`](/reference/exchange-rate-list) — текущие курсы валют к USDT. *** ## Приём платежей * [Объект платежа (Payment)](/reference/payment-object) — структура и статусы `payment_status`. | Метод | Назначение | | ------------------------------------------------------------------------- | -------------------------------------------------- | | [`POST /v1/payment`](/reference/payment-create) | Создание платёжного счёта (инвойса) | | [`POST /v1/payment/batch`](/reference/payment-batch) | **Массовое создание счетов** (до 5000, асинхронно) | | [`POST /v1/payment/info`](/reference/payment-info) | Информация о счёте (включая `refunds[]`) | | [`POST /v1/payment/history`](/reference/payment-history) | Список платежей мерчанта | | [`POST /v1/payment/send-email`](/reference/payment-send-email) | Отправить счёт письмом покупателю | | [`POST /v1/payment/services`](/reference/payment-services) | Доступные методы приёма | | [`POST /v1/payment/qr`](/reference/payment-qr) | QR‑код депозит‑адреса счёта | | [`POST /v1/wallet/qr`](/reference/wallet-qr) | QR‑код произвольного адреса | | [`POST /v1/payment/accepted/list · /set`](/reference/payment-accepted) | Принимаемые валюты для агностичных счетов | | [`POST /v1/payment/discount/list · /set`](/reference/payment-discount) | Скидки/наценки плательщику | | [`POST /v1/payment/accuracy/get · /set`](/reference/payment-accuracy) | Допуск недоплаты | | [`POST /v1/payment/autorefund/get · /set`](/reference/payment-autorefund) | Автовозврат при недо/переплате | | [`POST /v1/vrcs`](/reference/vrcs) | Авто‑конвертация волатильных депозитов в USDT | ### Платёжные ссылки Многоразовый URL оплаты: фиксированная сумма, донат (сумму вводит покупатель) или диапазон. **Единственный способ принимать платежи без своего бэкенда** (Tilda, Wix и т. п.). | Метод | Назначение | | ---------------------------------------------------------------------------------- | ------------------------------ | | [`POST /v1/payment/link` · `/list` · `/info` · `/toggle`](/reference/payment-link) | Создание и управление ссылками | ### Hosted‑страница оплаты и страница ссылки (публичные) | Метод | Назначение | | ------------------------------------------------------- | ---------------------------------- | | [`GET /v1/pay/{id}`](/reference/pay-get) | Публичные данные счёта | | [`POST /v1/pay/{id}/select`](/reference/pay-select) | Выбор валюты и сети покупателем | | [`GET /v1/link/{id}`](/reference/link-public) | Публичные данные платёжной ссылки | | [`POST /v1/link/{id}/checkout`](/reference/link-public) | Создать счёт по ссылке (без ключа) | *** ## Массовые операции (батчи) До **5000** элементов в одном подписанном запросе, обработка асинхронная. **Штатный способ не упираться в [лимит частоты](/reference/basics-ratelimit)** — один запрос вместо тысяч. | Метод | Назначение | | ---------------------------------------------------- | -------------------------------------------- | | [`POST /v1/payment/batch`](/reference/payment-batch) | Массовое создание счетов | | [`POST /v1/refund/batch`](/reference/refund-batch) | Массовые возвраты | | [`POST /v1/payout/batch`](/reference/payout-batch) | Массовые выплаты | | [`POST /v1/batch/info`](/reference/batch-info) | Статус батча и результат по каждому элементу | *** ## Сплиты Доля каждого входящего платежа автоматически уходит партнёру — для партнёрских программ, маркетплейсов, разделения выручки. | Метод | Назначение | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------ | | [`POST /v1/split/rule` · `/rule/list` · `/rule/delete`](/reference/split-rule) | Правила распределения (внешний адрес или мерчант платформы) | | [`POST /v1/split/config/get · /set`](/reference/split-config) | Окно удержания `refund_hold_hours` (и почему расчёт откладывается) | *** ## Кошельки, баланс, рефералы * [Объект кошелька (Wallet)](/reference/wallet-object) — статический адрес и чем он отличается от инвойса. | Метод | Назначение | | ---------------------------------------------------------------------------- | ------------------------------------ | | [`POST /v1/wallet`](/reference/wallet-create) | Создать статический кошелёк | | [`POST /v1/wallet/block`](/reference/wallet-block) | Заблокировать/разблокировать кошелёк | | [`POST /v1/wallet/blocked-address-refund`](/reference/wallet-blocked-refund) | Возврат средств с кошелька | | [`POST /v1/balance`](/reference/balance) | Балансы мерчанта | | [`POST /v1/referral/info`](/reference/referral-info) | Реферальная статистика | *** ## Выплаты и возвраты * [Объект выплаты (Payout)](/reference/payout-object) — структура и статусы. | Метод | Назначение | | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | [`POST /v1/payout`](/reference/payout-create) | Создать выплату | | [`POST /v1/payout/batch`](/reference/payout-batch) | **Массовая выплата (до 5000, асинхронно)** — основной путь | | [`POST /v1/payout/mass`](/reference/payout-mass) | Массовая выплата (до 100, синхронно) — легаси | | [`POST /v1/payout/info`](/reference/payout-info) | Информация о выплате | | [`POST /v1/payout/history`](/reference/payout-history) | История выплат | | [`POST /v1/payout/services`](/reference/payout-services) | Доступные методы выплат | | [`POST /v1/payout/calculate`](/reference/payout-calculate) | Предрасчёт комиссии и сумм | | [`POST /v1/payout/approve`](/reference/payout-approve) | Подтверждение выплаты (maker‑checker) | | [`POST /v1/payment/refund`](/reference/payment-refund) | Возврат платежа (адрес необязателен) | | [`POST /v1/refund/batch`](/reference/refund-batch) | Массовые возвраты (до 5000) | | [`POST /v1/payout/fee-config/get · /set`](/reference/payout-fee-config) | Кто платит сетевую комиссию выплаты | | [`POST /v1/payout/refund-fee-config/get · /set`](/reference/payout-refund-fee-config) | Кто платит комиссию возврата | | [`POST /v1/transfer/to-personal`](/reference/transfer-to-personal) | Перевод на личный кошелёк владельца | *** ## Вебхуки * [Объект вебхука и проверка подписи](/reference/webhook-object) — заголовки, формат тела, HMAC вебхука. | Метод | Назначение | | ---------------------------------------------------------------------------- | ---------------------------- | | [`POST /v1/webhooks`](/reference/webhooks-register) | Регистрация URL для вебхуков | | [`POST /v1/webhooks/deliveries`](/reference/webhooks-deliveries) | Журнал доставок | | [`POST /v1/payment/resend`](/reference/payment-resend) | Переотправка вебхука платежа | | [`POST /v1/testing-webhook`, `/v1/test-webhook/*`](/reference/webhooks-test) | Тестовые вебхуки | ## Настройки аккаунта | Метод | Назначение | | ------------------------------------------------------ | -------------------------- | | [`POST /v1/auto-withdraw/*`](/reference/auto-withdraw) | Автовывод на внешний адрес | | [`POST /v1/api-allowlist/*`](/reference/api-allowlist) | IP‑allowlist для API | # GET /v1/pay/{id} Source: https://docs.oblodai.com/reference/pay-get Публичные данные одного счёта: адрес, сумма, QR, статус, срок. Обслуживает hosted‑страницу оплаты, на которую вы отправляете покупателя по ссылке `url` из ответа [создания счёта](/reference/payment-create). **Публичный метод.** Аутентификация не требуется — отдаёт только клиентские поля (без `additional_data`, `payer_email` и `payer_address`). Страница может опрашивать его для отслеживания статуса **без секрета мерчанта в браузере**. **URL:** `https://api.oblodai.com/v1/pay/{id}` · **Метод:** `GET`. *** ## Параметры пути | Параметр | Описание | | -------- | ---------------------------------- | | `{id}` | `uuid` счёта (из ответа создания). | *** ## Пример запроса ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e ``` ```python Python theme={null} import requests r = requests.get("https://api.oblodai.com/v1/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e") print(r.json()) ``` ```js Node.js theme={null} const res = await fetch("https://api.oblodai.com/v1/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e"); console.log(await res.json()); ``` *** ## Пример ответа Публичный объект платежа: `address`, `payer_amount`, `payer_currency`, `address_qr_code`, `payment_status`, `expired_at` и прочие клиентские поля (без приватных `additional_data`, `payer_email` и `payer_address`). Полное описание полей — [Объект платежа](/reference/payment-object). ```json Счёт с выбранной валютой theme={null} { "state": 0, "result": { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "order_id": "order-1", "amount": "10.00", "payment_amount": null, "amount_paid": "0", "amount_remaining": "10.150000", "payer_amount": "10.150000", "payer_currency": "USDT", "currency": "USD", "network": "tron", "address": "TJ4b1...C9xk", "address_qr_code": "data:image/png;base64,iVBORw0KGgo...", "payment_status": "check", "is_multi": false, "url": "https://pay.oblodai.com/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "expired_at": 1783728000, "is_final": false, "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:00:00Z", "url_return": "", "url_success": "", "rate_expires_at": 1783724700, "confirmations": 0, "required_confirmations": 20, "txid": "" } } ``` ```json Валюто-агностичный (status select) theme={null} { "state": 0, "result": { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "order_id": "order-1", "amount": "10.00", "payment_amount": null, "amount_paid": "", "amount_remaining": "", "payer_amount": "", "payer_currency": "", "currency": "USD", "network": "", "address": "", "address_qr_code": "", "payment_status": "select", "is_multi": true, "url": "https://pay.oblodai.com/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "expired_at": 1783728000, "is_final": false, "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:00:00Z", "url_return": "", "url_success": "", "rate_expires_at": 1783724700, "confirmations": 0, "required_confirmations": 20, "txid": "", "accepted": [ { "currency": "USDT", "network": "tron" }, { "currency": "ETH", "network": "ethereum" } ] } } ``` Для **валюто‑агностичного** счёта в статусе `select` в ответ добавляется массив `accepted` — методы, доступные покупателю на выбор. После выбора методом [`POST /v1/pay/{id}/select`](/reference/pay-select) появляются реальные `address` и `payer_amount`. Только при `payment_status: "select"`. Элементы — пары `{ "currency": "...", "network": "..." }`: набор [`accepted`](/reference/payment-accepted) мерчанта, а если он не настроен — полный каталог методов. *** ## Коды ошибок | Код | Значение | | ------------------- | -------------------- | | `400 pay.bad_uuid` | Некорректный `{id}`. | | `404 pay.not_found` | Счёт не найден. | *** ## Связанные страницы # POST /v1/pay/{id}/select Source: https://docs.oblodai.com/reference/pay-select Покупатель выбирает валюту и сеть для **валюто‑агностичного** счёта (счёт без заранее выбранной валюты — покупатель выбирает монету и сеть на странице оплаты; см. [Три режима создания счёта](/guides/payment-modes)): метод фиксирует курс, выделяет депозит‑адрес и переводит счёт из статуса `select` в `check`. **Публичный метод.** Аутентификация не требуется — вызывается со страницы оплаты. **URL:** `https://api.oblodai.com/v1/pay/{id}/select` · **Метод:** `POST`. *** ## Параметры Выбранная валюта оплаты. Выбранная сеть. `{id}` в пути — `uuid` счёта. *** ## Пример запроса ```bash cURL theme={null} curl -s https://api.oblodai.com/v1/pay/8b1d7d2e-…/select \ -X POST -H 'Content-Type: application/json' \ -d '{"currency":"USDT","network":"tron"}' ``` ```python Python theme={null} import requests r = requests.post("https://api.oblodai.com/v1/pay/8b1d7d2e-…/select", json={"currency": "USDT", "network": "tron"}) print(r.json()) ``` ```js Node.js theme={null} const res = await fetch("https://api.oblodai.com/v1/pay/8b1d7d2e-…/select", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ currency: "USDT", network: "tron" }), }); console.log(await res.json()); ``` *** ## Пример ответа Обновлённый публичный объект платежа — с реальным `address`, `payer_amount`, `address_qr_code` (тот же формат, что у [`GET /v1/pay/{id}`](/reference/pay-get), без приватных `additional_data`, `payer_email` и `payer_address`). Счёт переходит в `payment_status: "check"`, `is_multi` становится `false` (до выбора был `true`). ```json theme={null} { "state": 0, "result": { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "order_id": "order-1", "amount": "10.00", "payment_amount": null, "amount_paid": "0", "amount_remaining": "10.150000", "payer_amount": "10.150000", "payer_currency": "USDT", "currency": "USD", "network": "tron", "address": "TJ4b1...C9xk", "address_qr_code": "data:image/png;base64,iVBORw0KGgo...", "payment_status": "check", "is_multi": false, "url": "https://pay.oblodai.com/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "expired_at": 1783728000, "is_final": false, "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:05:00Z", "url_return": "", "url_success": "", "rate_expires_at": 1783725000, "confirmations": 0, "required_confirmations": 20, "txid": "" } } ``` *** ## Коды ошибок | Код | Значение | | --------------------------------- | ------------------------------------------------------------------ | | `400 pay.not_selectable` | Валюта уже выбрана (счёт не в статусе `select`). | | `400 pay.unknown_currency` | Неизвестная валюта. | | `400 pay.method_not_accepted` | Метод не входит в набор [`accepted`](/reference/payment-accepted). | | `400 payment.unsupported_network` | Пара валюта+сеть не поддерживается. | | `400 pay.below_minimum` | Сумма ниже минимума сети. | *** ## Нюансы * Выбранный метод должен быть в наборе [`accepted`](/reference/payment-accepted) (или в полном каталоге, если набор пуст) **и** поддерживаться каталогом для этой валюты. * Курс и адрес фиксируются **в момент выбора**. Комиссия при этом уже была зафиксирована при создании счёта. *** ## Связанные страницы # Принимаемые валюты Source: https://docs.oblodai.com/reference/payment-accepted **Методы:** `/v1/payment/accepted/list` · `/v1/payment/accepted/set` Управление набором принимаемых валют и сетей для **валюто‑агностичных** счетов (счёт без заранее выбранной валюты — покупатель выбирает монету и сеть на странице оплаты; см. [Три режима создания счёта](/guides/payment-modes)). Именно из этого набора покупатель выбирает валюту на [hosted‑странице](/reference/pay-select). **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## POST /v1/payment/accepted/list Показать текущий набор — пустое тело `{}` без параметров. ```json theme={null} { "state": 0, "result": { "accepted": [ { "currency": "USDT", "network": "tron" } ] } } ``` **Пустой список = «принимаю всё»** — покупателю доступен весь каталог методов. *** ## POST /v1/payment/accepted/set Полностью **заменяет** набор (не merge — приходит новый список целиком). Массив объектов `{ currency, network }`. Код валюты. Код сети. ### Пример запроса ```bash cURL theme={null} BODY='{"accepted":[{"currency":"USDT","network":"tron"},{"currency":"USDC","network":"polygon"}]}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/accepted/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/accepted/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/accepted/set", {"accepted": [ {"currency": "USDT", "network": "tron"}, {"currency": "USDC", "network": "polygon"}, ]}) ``` ```js Node.js theme={null} await call("/v1/payment/accepted/set", { accepted: [ { currency: "USDT", network: "tron" }, { currency: "USDC", network: "polygon" }, ] }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "ok": true } } ``` ### Коды ошибок | Код | Значение | | ------------------------------- | ----------------------------- | | `400 accepted.unknown_currency` | Неизвестная валюта в наборе. | | `400 accepted.no_network` | Не указана сеть для элемента. | *** ## Нюансы * `/set` **заменяет** весь набор, а не добавляет к нему. Чтобы «принимать всё», отправьте пустой массив. * Выбор покупателя на [`POST /v1/pay/{id}/select`](/reference/pay-select) валидируется против этого набора (или против полного каталога, если набор пуст). *** ## Связанные страницы # Допуск недоплаты Source: https://docs.oblodai.com/reference/payment-accuracy **Методы:** `/v1/payment/accuracy/get` · `/v1/payment/accuracy/set` Допуск недоплаты/переплаты. Счёт, отклонившийся от суммы не более чем на `accuracy_percent`, всё равно засчитывается как оплаченный (`paid`). **По умолчанию (без явной настройки) действует допуск \~1 %** — см. примечание ниже про то, как потребовать ровную сумму. **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## POST /v1/payment/accuracy/get Тело — `{}`. ```json theme={null} { "state": 0, "result": { "enabled": true, "accuracy_percent": 2 } } ``` Для аккаунта, который **ни разу не настраивал** допуск, `get` вернёт `{ "enabled": false, "accuracy_percent": 0 }`. **Это НЕ означает «ровная сумма».** Пока настройка не сохранена явно, счета используют допуск по умолчанию **\~1 %** (недоплата/переплата в пределах \~1 % засчитывается как `paid`). Чтобы требовать точную сумму (нулевой допуск), нужно **явно** сохранить настройку выключенной — `set {"enabled": false}`. То есть «никогда не настраивал» (≈1 %) и «сохранил выключенной» (0 %) — разное поведение. *** ## POST /v1/payment/accuracy/set Включить/выключить допуск. Допуск в процентах, 1–5. Обязателен при `enabled: true`; при `enabled: false` игнорируется (значение сбрасывается в 0). Кэп 5 %. ### Пример запроса ```bash cURL theme={null} BODY='{"enabled":true,"accuracy_percent":2}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/accuracy/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/accuracy/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/accuracy/set", {"enabled": True, "accuracy_percent": 2}) ``` ```js Node.js theme={null} await call("/v1/payment/accuracy/set", { enabled: true, accuracy_percent: 2 }); ``` *** ## Коды ошибок | Код | Значение | | --------------------------- | -------------------------------------------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 accuracy.out_of_range` | При `enabled: true` значение `accuracy_percent` вне диапазона 1–5 % (в т. ч. 0). | *** ## Нюансы * Кэп допуска — **5 %**. * Per‑request поле `accuracy_payment_percent` в [`POST /v1/payment`](/reference/payment-create) **перекрывает** эту настройку для конкретного счёта. * Что делать с недоплатой, которая **не** попала в допуск, — регулирует [автовозврат](/reference/payment-autorefund). *** ## Связанные страницы # Автовозврат Source: https://docs.oblodai.com/reference/payment-autorefund **Методы:** `/v1/payment/autorefund/get` · `/v1/payment/autorefund/set` Авто‑возврат средств плательщику при платежах **вне допуска**: возврат излишка при переплате и возврат суммы при истёкшей недоплате. **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## POST /v1/payment/autorefund/get Тело — `{}`. ```json theme={null} { "state": 0, "result": { "overpay": true, "underpay": true, "configured": false } } ``` Оба флага по умолчанию **включены**. `configured: false` означает, что явной записи нет и возвращён дефолт. *** ## POST /v1/payment/autorefund/set Возвращать излишек при переплате (`paid_over`). Возвращать средства при истёкшей недоплате (`wrong_amount`). ### Пример запроса ```bash cURL theme={null} BODY='{"overpay":true,"underpay":true}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/autorefund/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/autorefund/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/autorefund/set", {"overpay": True, "underpay": True}) ``` ```js Node.js theme={null} await call("/v1/payment/autorefund/set", { overpay: true, underpay: true }); ``` *** ## Нюансы * Возврат идёт на **адрес плательщика**. **По умолчанию из суммы удерживаются и наша комиссия, и сетевой газ** (авто-возвраты по умолчанию «за счёт клиента»). То есть плательщик получает `сумма − наша комиссия − газ`. Кто несёт комиссию возврата — настраивается на уровне проекта через [refund-fee-config](/reference/payout-refund-fee-config); можно переключить на «за счёт мерчанта». (Важно: у **ручного** [`/v1/payment/refund`](/reference/payment-refund) дефолт обратный — за счёт мерчанта.) * Поддержаны сети **EVM, Tron, TON, Solana**. Для **Bitcoin/UTXO** возврат ручной. * Взаимодействие с допуском: сначала работает [`accuracy`](/reference/payment-accuracy) (недоплата в пределах допуска считается оплатой), а автовозврат обрабатывает то, что в допуск не попало. *** ## Связанные страницы # POST /v1/payment/batch Source: https://docs.oblodai.com/reference/payment-batch Создать **много счетов одним запросом** — до **5000** штук. Обрабатывается асинхронно: в ответ сразу приходит `batch_id`, а результат по каждому элементу забирается через [`POST /v1/batch/info`](/reference/batch-info). **Это штатный способ не упираться в rate limit.** Один подписанный запрос вместо 5000 — [лимит частоты](/reference/basics-ratelimit) считается по запросам, а не по элементам внутри них. Если вам нужно выставить тысячу счетов, делайте это батчем, а не тысячей вызовов [`POST /v1/payment`](/reference/payment-create). **URL:** `https://api.oblodai.com/v1/payment/batch` · **Аутентификация:** обязательна · **Идемпотентность:** заголовок `Idempotency-Key` (на весь батч) + `order_id` каждого элемента. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Массив от 1 до **5000** элементов. Поля каждого элемента — **ровно те же**, что у [`POST /v1/payment`](/reference/payment-create). Что делать при ошибке элемента: `continue` (по умолчанию) — обрабатывать остальные; `stop` — прекратить обработку после первой ошибки. Каждый элемент проходит **тот же** путь создания, что и одиночный `POST /v1/payment`: те же проверки, та же комиссия, та же идемпотентность по `order_id`. Задавайте `order_id` элементам — по нему потом удобно сопоставлять результаты, и он же защищает от дублей при повторной отправке батча. *** ## Пример запроса ```bash cURL theme={null} BODY='{"payments":[ {"amount":"10","currency":"USD","order_id":"order-1","to_currency":"USDT","network":"tron"}, {"amount":"25","currency":"EUR","order_id":"order-2","to_currency":"USDT","network":"tron"} ],"on_error":"continue"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/batch' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/batch \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -H "Idempotency-Key: batch-2026-07-13-a" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/batch", { "payments": [ {"amount": "10", "currency": "USD", "order_id": "order-1", "to_currency": "USDT", "network": "tron"}, {"amount": "25", "currency": "EUR", "order_id": "order-2", "to_currency": "USDT", "network": "tron"}, ], "on_error": "continue", }) ``` ```js Node.js theme={null} await call("/v1/payment/batch", { payments: [ { amount: "10", currency: "USD", order_id: "order-1", to_currency: "USDT", network: "tron" }, { amount: "25", currency: "EUR", order_id: "order-2", to_currency: "USDT", network: "tron" }, ], on_error: "continue", }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "batch_id": "9f4c1a2b-77de-4a55-9c1f-0e2b3d4a5f60", "kind": "payment", "count": 2, "status": "pending" } } ``` Идентификатор батча. С ним идите в [`POST /v1/batch/info`](/reference/batch-info). Вид батча — здесь всегда `payment`. Сколько элементов принято в обработку. Стартовый статус — всегда `pending`. **Счетов в ответе нет.** Ответ подтверждает только приём батча в очередь. Сами счета (их `uuid`, `address`, `url`) появятся в `items[].result` ответа [`/v1/batch/info`](/reference/batch-info) по мере обработки. *** ## Статусы батча `pending` → `processing` → `completed`. | Статус | Значение | | ------------ | ---------------------------------------------------------------- | | `pending` | Батч принят, воркер ещё не начал. | | `processing` | Элементы обрабатываются. | | `completed` | Обработка закончена: каждый элемент дошёл до `done` или `error`. | **`completed` — это «обработка закончена», а НЕ «всё успешно».** Батч, где часть счетов не создалась, тоже приходит в `completed`. Смотрите на `succeeded` / `failed` и на `items[].error` в ответе [`/v1/batch/info`](/reference/batch-info). *** ## Коды ошибок | Код | Значение | | ---------------------- | ---------------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 batch.empty` | Массив `payments` пуст. | | `400 batch.too_large` | Больше 5000 элементов. Разбейте на несколько батчей. | | `503 batch.disabled` | Массовая обработка недоступна на этом шлюзе. | | `401 auth.*` | Ошибки аутентификации. | Ошибки **отдельных элементов** сюда не попадают: батч принимается целиком, а ошибка конкретного счёта приходит в `items[].error` ответа [`/v1/batch/info`](/reference/batch-info). *** ## Нюансы * **Асинхронность.** Ответ приходит мгновенно, до фактического создания счетов. Не считайте счета созданными по ответу на submit — поллите [`/v1/batch/info`](/reference/batch-info). * **Цена элемента может быть в фиате.** В одном батче спокойно уживаются счета с ценой в `RUB`, `EUR`, `USD` и в монетах — правила те же, что у одиночного [`POST /v1/payment`](/reference/payment-create). * **`on_error: "stop"`.** После первой ошибки остальные элементы не обрабатываются: они получают `status: "error"` с сообщением `"skipped: batch stopped after an earlier failure"` и **попадают в счётчик `failed`**. Разберитесь с причиной и отправьте остаток новым батчем. * **Идемпотентность двухуровневая.** Заголовок `Idempotency-Key` защищает от повторной отправки всего батча; `order_id` внутри элемента — от дубля конкретного счёта. * **Лимит частоты.** Батч — это **один** запрос в бюджете [rate limit](/reference/basics-ratelimit), сколько бы элементов в нём ни было. *** ## Связанные страницы поллинг статуса и результатов. поля элемента. # POST /v1/payment Source: https://docs.oblodai.com/reference/payment-create Создаёт платёжный счёт (инвойс). Поддерживает три режима: фиксированная валюта + сеть, авто‑нормализация сети (если у валюты она одна) и валюто‑агностичный счёт (валюту и сеть выбирает покупатель на hosted‑странице). **URL:** `https://api.oblodai.com/v1/payment` · **Аутентификация:** обязательна ([подпись](/reference/basics-auth)) · **Идемпотентность:** заголовок `Idempotency-Key` и/или `order_id`. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Сумма к оплате в валюте `currency`. Код **валюты цены**. Это **любой из 23 фиатов** (`USD`, `EUR`, `GBP`, `RUB`, `UAH`, `JPY`, …) **или любая монета** (`USDT`, `BTC`, …). Полный список — `pricing_currencies` из [`GET /v1/currencies`](/reference/currencies). У `JPY` и `KRW` **ноль знаков** после запятой (`"10000"`, а не `"10000.00"`). Ссылка мерчанта; ключ идемпотентности. Настоятельно рекомендуется. Сеть расчёта (напр. `tron`, `ethereum`). См. режимы ниже. **Валюта расчёта** — крипта, которой платят (фиат здесь невозможен). По умолчанию `= currency`, но **только если `currency` — крипта**. Если цена в фиате, задайте `to_currency` явно — либо не задавайте и `network`, тогда монету выберет покупатель. Время жизни счёта, сек; 300–43200. По умолчанию 3600. Значения вне диапазона **обрезаются** к ближайшей границе (не ошибка). **Устаревшее; новичку не нужно** — payer‑facing наценки настраиваются через [`discount`](/reference/payment-discount). % сетевой наценки на плательщика (0–100). Допуск недо/переплаты, 0–5 %. Перекрывает настройку мерчанта. Индивидуальный webhook для этого счёта. Ссылка «назад в магазин» на странице оплаты. Редирект после успешной оплаты. Приватные данные мерчанта, эхом в вебхуках (покупателю не видны). Email плательщика. Если задан — **после оплаты на него автоматически уходит чек**. Он же получатель по умолчанию у [`POST /v1/payment/send-email`](/reference/payment-send-email). Тема страницы оплаты: `dark` | `light`. Разрешить доплату остатка. Оживить просроченный счёт по `order_id` вместо создания нового. Идемпотентность обеспечивает заголовок `Idempotency-Key` **или** `order_id`. Если нет **ни того, ни другого**, каждый вызов создаёт новый счёт — задавайте хотя бы одно из двух. **Минимально необходимое:** `amount`, `currency`; настоятельно рекомендуем также `order_id` или заголовок `Idempotency-Key`. Остальные поля — по мере надобности. ### Заголовки Любое уникальное значение (≤255 символов), одинаковое во всех повторах одного создания. Повтор вернёт тот же счёт и заголовок `Idempotent-Replayed: true`. Работает независимо от `order_id` и вместе с ним. → [Идемпотентность](/reference/basics-idempotency) *** ## Режимы выбора валюты и сети **Валюта цены** (`currency`) и **валюта расчёта** (`to_currency`) — разные вещи: цену можно назначить в фиате (`USD`, `EUR`, `RUB`, … — [23 валюты](/reference/basics-money)) или в крипте, а платят **всегда криптой**. Подробнее — [Форматы сумм и денег](/reference/basics-money). Правила ниже одинаковы для **любого фиата**, не только для `USD`. | `currency` (цена) | `to_currency` / `network` | Поведение | | -------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | крипта (`USDT`, `BTC`, …) | оба пусты | Валюто‑агностичный (deferred) счёт: монету и сеть выбирает покупатель на [hosted‑странице](/reference/pay-select). | | **фиат (`USD`, `EUR`, …)** | **оба пусты** | **Валюто‑агностичный счёт: цена в фиате, монету и сеть выбирает покупатель.** Основной сценарий. | | фиат | `to_currency` + `network` заданы | Обычный счёт: цена в фиате, оплата в выбранной вами монете. | | крипта | `to_currency` + `network` заданы | Обычный счёт под конкретную монету и сеть. | | крипта | `to_currency` пуст, `network` задан | `to_currency` по умолчанию `= currency`. Работает, **только когда цена в крипте**. | | любая | `to_currency` задан, `network` пуст | Авто‑сеть: если у монеты **ровно одна** сеть — подставится автоматически; если сетей несколько — ошибка `payment.network_required`. | | **фиат** | **`to_currency` пуст, `network` задан** | **Ошибка `400 payment.to_currency_required`** — из цены в фиате монету расчёта вывести нельзя. Задайте `to_currency` явно либо уберите и `network` (тогда монету выберет покупатель). | В валюто‑агностичном режиме курс и адрес фиксируются в момент выбора монеты покупателем, комиссия — уже при создании. Подробный разбор — [Три режима создания счёта](/guides/payment-modes). *** ## Пример запроса ```bash cURL wrap theme={null} BODY='{"amount":"10","currency":"USD","order_id":"order-1","to_currency":"USDT","network":"tron","lifetime":3600}' TS=$(date +%s) SIGSTR=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment' "$BODY") SIG=$(printf '%s' "$SIGSTR" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment \ -X POST \ -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} # функция call(...) — из раздела «Аутентификация» call("/v1/payment", { "amount": "10", "currency": "USD", "order_id": "order-1", "to_currency": "USDT", "network": "tron", "lifetime": 3600, }) ``` ```js Node.js theme={null} // функция call(...) — из раздела «Аутентификация» await call("/v1/payment", { amount: "10", currency: "USD", order_id: "order-1", to_currency: "USDT", network: "tron", lifetime: 3600, }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "order_id": "order-1", "amount": "10.00", "payment_amount": null, "amount_paid": "0", "amount_remaining": "10.150000", "payer_amount": "10.150000", "payer_currency": "USDT", "currency": "USD", "network": "tron", "address": "TJ4b1...C9xk", "address_qr_code": "data:image/png;base64,iVBORw0KGgo...", "payment_status": "check", "is_multi": false, "url": "https://pay.oblodai.com/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "expired_at": 1783728000, "is_final": false, "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:00:00Z", "additional_data": "", "payer_email": "", "url_return": "", "url_success": "", "rate_expires_at": 1783724700, "confirmations": 0, "required_confirmations": 20, "txid": "" } } ``` `payer_amount` — это `amount`, пересчитанная в крипту по текущему курсу (плюс, если задан `subtract`, сетевая наценка на плательщика). Поэтому число отличается от `amount`. Полное описание всех полей — [Объект платежа](/reference/payment-object). Для валюто‑агностичного счёта `is_multi: true`, а `payer_currency`, `address`, `address_qr_code`, `payer_amount`, `amount_paid`, `amount_remaining` пусты до выбора монеты покупателем — валюты расчёта у такого счёта ещё просто нет. *** ## Коды ошибок | Код | Значение | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 payment.unknown_currency` | Неизвестная `currency`. | | `400 payment.unknown_to_currency` | Неизвестная `to_currency`. | | `400 payment.to_currency_required` | Цена в фиате (`USD`, `EUR`, `RUB`, …), задана `network`, но не задана `to_currency`: монету расчёта из фиата вывести нельзя. | | `400 payment.bad_url_callback` | `url_callback` не является валидным http(s)-URL (или указывает на приватный/метаданные-адрес). | | `400 payment.network_required` | У валюты несколько сетей, `network` не задан. | | `400 payment.unsupported_network` | Пара валюта+сеть не поддерживается. | | `400 payment.bad_amount` | Некорректная сумма. | | `400 payment.bad_subtract` | `subtract` вне 0–100. | | `400 payment.bad_accuracy` | `accuracy_payment_percent` вне 0–5. | | `400 payment.below_minimum` | Сумма ниже минимума сети. | | `503 invoice.fee_quote_failed` | Не удалось оценить по курсу фиксированную часть комиссии — счёт не создан. Повторите с backoff. | | `401 auth.*` | Ошибки аутентификации. | *** ## Нюансы * **Идемпотентность — два механизма, работают вместе.** Заголовок `Idempotency-Key` (рекомендуемый): повтор с тем же значением вернёт **тот же** ответ и `Idempotent-Replayed: true`. И `order_id`: повтор (или переигранный в пределах 5‑мин окна подписи запрос) с `order_id`, для которого уже есть **живой** счёт, вернёт этот же счёт, а не создаст дубль. Терминальный или просроченный счёт создание нового не блокирует. Проверка‑и‑создание защищены advisory‑локом по паре `(merchant, order_id)` от гонки двух одновременных запросов. → [Идемпотентность](/reference/basics-idempotency) * **Цена в фиате.** `currency` может быть любой из 23 фиатных валют (`USD`, `EUR`, `GBP`, `RUB`, `UAH`, `PLN`, `CZK`, `TRY`, `CNY`, `INR`, `BRL`, `CAD`, `AUD`, `CHF`, `AED`, `ZAR`, `MXN`, `IDR`, `THB`, `VND`, `NGN`, `JPY`, `KRW`) — курс берётся напрямую в этой валюте. Тенге (`KZT`), сом (`KGS`) и сум (`UZS`) **пока не поддерживаются** — вернётся `payment.unknown_currency`. * **Много счетов сразу** — [`POST /v1/payment/batch`](/reference/payment-batch): до 5000 за один запрос, один «тик» [лимита частоты](/reference/basics-ratelimit). * **`is_refresh`.** С `is_refresh: true` и существующим `order_id` просроченный счёт **оживляется** (перезакрепляется курс, продлевается lifetime). Если такого счёта нет — обычное создание. * **Курс и его окно.** Курс фиксируется при создании; `rate_expires_at` — до какого момента он действителен. Для долгоживущей ссылки курс лениво перезапрашивается при открытии hosted‑страницы, пока счёт не оплачен и не истёк (адрес и сумма после начала оплаты не меняются). * **Минимум сети.** Платёж ниже минимума сети в USD отклоняется (`payment.below_minimum`). На дорогих сетях (Ethereum, Bitcoin) минимум есть, на дешёвых — нет. Если курса нет, минимум не применяется. * **Комиссия.** Наша комиссия = эффективная ставка мерчанта (его закреплённый override либо глобальная платформенная, по умолчанию **1.5 % + \$0.30 с платежа** — фиксированная часть «размазывается» в ставку при фиксации курса). Устаревший `subtract` **не** задаёт нашу ставку — это только наценка сети на плательщика. * **Подтверждения.** `required_confirmations` подбирается под сумму: мелкий платёж может подтвердиться за меньшее число подтверждений, крупный требует полного reorg‑безопасного порога. *** ## Связанные страницы много счетов одним запросом. многоразовая ссылка оплаты (в т. ч. без бэкенда). отправить счёт письмом. # Скидки и наценки Source: https://docs.oblodai.com/reference/payment-discount **Методы:** `/v1/payment/discount/list` · `/v1/payment/discount/set` Скидка или наценка плательщику на чекауте в зависимости от выбранной монеты. Положительный процент — **скидка** за оплату этой монетой; отрицательный — **наценка**. **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## POST /v1/payment/discount/list Показать текущие правила — пустое тело `{}` без параметров. ```json theme={null} { "state": 0, "result": [ { "currency": "USDT", "network": "tron", "discount_percent": 3 } ] } ``` Каждый элемент: `currency`, `network`, `discount_percent`. *** ## POST /v1/payment/discount/set Валюта. Пусто = глобальный дефолт для всех монет. Сеть. Пусто = любая сеть данной валюты. Процент, −99…99. Плюс — скидка, минус — наценка. ### Пример запроса ```bash cURL theme={null} BODY='{"currency":"USDT","network":"tron","discount_percent":3}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/discount/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/discount/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/discount/set", {"currency": "USDT", "network": "tron", "discount_percent": 3}) ``` ```js Node.js theme={null} await call("/v1/payment/discount/set", { currency: "USDT", network: "tron", discount_percent: 3 }); ``` ### Пример ответа Эхо сохранённого правила: ```json theme={null} { "state": 0, "result": { "currency": "USDT", "network": "tron", "discount_percent": 3 } } ``` ### Коды ошибок | Код | Значение | | ------------------------------- | ---------------------------------------- | | `400 discount.unknown_currency` | Неизвестная валюта. | | `400 discount.out_of_range` | `discount_percent` вне диапазона −99…99. | *** ## Нюансы * **100 % скидка запрещена** — дала бы нулевую цену. * Запись по конкретной монете **перекрывает** глобальный дефолт. * Скидка/наценка применяется к цене **до фиксации курса**. *** ## Связанные страницы # POST /v1/payment/history Source: https://docs.oblodai.com/reference/payment-history Список платежей мерчанта — новые сверху, с limit/offset‑пагинацией. **URL:** `https://api.oblodai.com/v1/payment/history` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Все поля опциональны. Размер страницы. По умолчанию 25, диапазон 1–100. Смещение от начала. Фильтр по статусу ([`payment_status`](/reference/payment-object#статусы-payment_status)). Пусто = все. *** ## Пример запроса ```bash cURL theme={null} BODY='{"limit":25,"offset":0,"status":"paid"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/history' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/history \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/history", {"limit": 25, "offset": 0, "status": "paid"}) ``` ```js Node.js theme={null} await call("/v1/payment/history", { limit: 25, offset: 0, status: "paid" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "items": [ { "uuid": "8b1d7d2e-…", "order_id": "order-1", "amount": "10.00", "payment_status": "paid", "is_final": true, "…": "полный объект платежа" } ], "paginate": { "count": 137, "per_page": 25, "offset": 0 } } } ``` Список объектов платежа — те же поля, что в [объекте платежа](/reference/payment-object), но без `refunds[]` и `refund_status` — они приходят только в [`/info`](/reference/payment-info) (в примере показаны не все). **Общее** число записей, не размер страницы. Размер страницы. Текущее смещение. *** ## Нюансы * `count` — общее число записей по фильтру. Для постраничного обхода увеличивайте `offset` на `limit`, пока не переберёте `count`. * Каждый элемент — объект платежа с теми же полями, что в [объекте платежа](/reference/payment-object), но без `refunds[]` и `refund_status` — они приходят только в [`POST /v1/payment/info`](/reference/payment-info); в примере выше поля сокращены для краткости. *** ## Связанные страницы # POST /v1/payment/info Source: https://docs.oblodai.com/reference/payment-info Возвращает актуальное состояние счёта по `uuid` или `order_id`. **URL:** `https://api.oblodai.com/v1/payment/info` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Нужен хотя бы один идентификатор; приоритет у `uuid`. Идентификатор счёта в Oblodai. Ваша ссылка на заказ. *** ## Пример запроса ```bash cURL theme={null} BODY='{"order_id":"order-1"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/info' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/info \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/info", {"order_id": "order-1"}) ``` ```js Node.js theme={null} await call("/v1/payment/info", { order_id: "order-1" }); ``` *** ## Ответ Тот же объект платежа, что у [`POST /v1/payment`](/reference/payment-create), с актуальными `payment_status`, `amount_paid`, `amount_remaining`, `confirmations`, `txid`, `payer_address`. Полное описание полей — [Объект платежа](/reference/payment-object). ### Возвраты: `refunds[]` и `refund_status` **Только этот метод** дополняет объект платежа сведениями о возвратах (в [`/history`](/reference/payment-history) их нет — это сэкономленный запрос на каждую строку списка): `none` — ничего не возвращали; `partial` — вернули часть; `full` — вернули всё оплаченное. Каждый возврат: `uuid`, `status`, `amount`, `address`, `txid`, `is_final`, `created_at`. ```json theme={null} { "state": 0, "result": { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "order_id": "order-1", "payment_status": "paid", "payer_currency": "USDT", "payer_address": "TQm9...7bXe", "refund_status": "partial", "refunds": [ { "uuid": "f9e8...01", "status": "paid", "amount": "5.000000", "address": "TQm9...7bXe", "txid": "0x9ab…", "is_final": true, "created_at": "2026-07-13T14:02:00Z" } ] } } ``` Отменённые и провалившиеся возвраты (`cancel`, `fail`) в `refund_status` не учитываются — денег они не вернули. По этой паре полей удобно проверять, не возвращён ли заказ, **не заводя своего учёта возвратов**. *** ## Коды ошибок | Код | Значение | | ----------------------- | ------------------------------------ | | `400 request.bad_json` | Тело не парсится. | | `400 payment.bad_uuid` | `uuid` некорректного формата. | | `400 payment.no_lookup` | Не передан ни `uuid`, ни `order_id`. | | `404 payment.not_found` | Счёт не найден. | *** ## Нюансы * Чужой счёт и несуществующий UUID отдают **одинаковый** `404 payment.not_found` — намеренно, чтобы нельзя было проверить существование чужого счёта. * Для отслеживания оплаты в реальном времени лучше полагаться на [вебхуки](/reference/webhook-object), а `/info` использовать как резервный опрос. *** ## Связанные страницы возврат (адрес по умолчанию — `payer_address`). # Платёжные ссылки Source: https://docs.oblodai.com/reference/payment-link **Методы:** `/v1/payment/link` · `/list` · `/info` · `/toggle` **Платёжная ссылка** — многоразовый URL оплаты, который вы просто даёте покупателю (в письме, в чате, кнопкой на сайте). Каждый, кто по ней платит, получает **свой отдельный счёт** — ссылку можно использовать сколько угодно раз. **Ссылка — единственный способ принимать платежи БЕЗ бэкенда.** Обычный [`POST /v1/payment`](/reference/payment-create) нужно подписывать секретом, а секрет нельзя класть в браузер — значит, нужен ваш сервер. У ссылки счёт создаёт **публичный** [`POST /v1/link/{id}/checkout`](/reference/link-public), подпись не нужна. Поэтому она работает на Tilda, Wix, в Notion, в письме — везде, где своего бэкенда нет. **Базовый URL:** `https://api.oblodai.com` · **Аутентификация:** обязательна (управление ссылками). Страница оплаты по ссылке — [публичные методы](/reference/link-public), без ключа. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Три режима суммы Главное решение при создании ссылки — кто задаёт сумму. | `amount_mode` | Кто задаёт сумму | Для чего | | ------------- | ------------------------------------------------------------ | ------------------------------------------------------------------- | | `fixed` | Вы — в `amount_fixed`. Покупатель изменить не может. | Товар, услуга, счёт с конкретной ценой. | | `open` | **Покупатель — любую.** `amount_min` — необязательный «пол». | **Донаты**, чаевые, «заплати сколько хочешь». | | `range` | Покупатель — в пределах `amount_min`…`amount_max`. | «От 5 до 1000 \$»: поддержка проекта с рамками, пополнение баланса. | *** ## POST /v1/payment/link — создать ссылку ### Параметры запроса Заголовок на странице оплаты. Описание на странице оплаты. Режим суммы: `fixed` | `open` | `range`. **Валюта цены** — фиат (`USD`, `EUR`, `RUB`, …) или монета. Список — `pricing_currencies` из [`GET /v1/currencies`](/reference/currencies). Сумма — для `fixed`. Обязательна в этом режиме. Нижняя граница: «пол» для `open` (необязателен) и минимум для `range` (обязателен). Верхняя граница — для `range`. Обязательна в этом режиме. **Валюта расчёта** (монета), закреплённая за ссылкой. Пусто — монету выбирает покупатель. Сеть расчёта, закреплённая за ссылкой. Пусто — сеть выбирает покупатель. Срок жизни ссылки, секунд от момента создания. **`0` (по умолчанию) — ссылка бессрочная.** Обязательность полей зависит от `amount_mode` — см. таблицу режимов выше. **Бессрочность.** `expires_in: 0` — ссылка живёт, пока вы её не выключите через `/toggle`. Это нормальный режим для кнопки «Поддержать» на сайте. Ограниченный срок нужен только под разовую акцию. **Закрепление монеты и сети.** Если задать `pinned_currency` + `pinned_network` — все платежи по ссылке пойдут в этой монете и сети. Если оставить пустыми — покупатель выберет монету и сеть сам (счёт создастся [валюто‑агностичным](/guides/payment-modes)). Промежуточный вариант (закрепить только сеть, оставив монету на выбор) смысла не имеет: закрепляйте либо оба поля, либо ни одного. ### Пример запроса ```bash cURL wrap theme={null} BODY='{"title":"Поддержать проект","description":"Спасибо!","amount_mode":"open","currency":"USD","amount_min":"1.00","expires_in":0}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/link' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/link \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} # Донат: сумму вводит покупатель, минимум $1, ссылка бессрочная, монету выбирает покупатель. call("/v1/payment/link", { "title": "Поддержать проект", "description": "Спасибо!", "amount_mode": "open", "currency": "USD", "amount_min": "1.00", "expires_in": 0, }) # Товар: фиксированные 25 EUR, оплата только USDT в Tron. call("/v1/payment/link", { "title": "Футболка", "amount_mode": "fixed", "currency": "EUR", "amount_fixed": "25.00", "pinned_currency": "USDT", "pinned_network": "tron", }) # Диапазон: от 5 до 1000 USD. call("/v1/payment/link", { "title": "Пополнение баланса", "amount_mode": "range", "currency": "USD", "amount_min": "5.00", "amount_max": "1000.00", }) ``` ```js Node.js theme={null} await call("/v1/payment/link", { title: "Поддержать проект", description: "Спасибо!", amount_mode: "open", currency: "USD", amount_min: "1.00", expires_in: 0, }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "link_id": "5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40", "url": "https://pay.oblodai.com/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40" } } ``` `url` — это и есть то, что вы даёте покупателю: ставите ссылкой на кнопку, шлёте в письме, кладёте в QR‑код. *** ## POST /v1/payment/link/list — список ссылок ### Параметры запроса Сколько ссылок вернуть. Смещение. ```python Python theme={null} call("/v1/payment/link/list", {"limit": 50, "offset": 0}) ``` ```js Node.js theme={null} await call("/v1/payment/link/list", { limit: 50, offset: 0 }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "items": [ { "link_id": "5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40", "title": "Поддержать проект", "description": "Спасибо!", "amount_mode": "open", "currency": "USD", "amount_min": "1.00", "active": true, "url": "https://pay.oblodai.com/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40", "created_at": "2026-07-13T12:00:00Z" } ] } } ``` Идентификатор ссылки. Что видит покупатель. Что видит покупатель. `fixed` | `open` | `range`. Валюта цены. Сумма при `fixed`. Присутствует, только если задана. Границы суммы. Присутствуют только те, что заданы. Границы суммы. Присутствуют только те, что заданы. Закреплённые монета и сеть. Отсутствуют, если выбирает покупатель. Закреплённые монета и сеть. Отсутствуют, если выбирает покупатель. Работает ли ссылка сейчас. Публичный URL страницы оплаты. Момент истечения (ISO 8601). **Отсутствует у бессрочной ссылки.** Время создания. *** ## POST /v1/payment/link/info — ссылка и её платежи ### Параметры запроса Идентификатор ссылки. Пагинация списка платежей. Смещение. ```python Python theme={null} call("/v1/payment/link/info", {"link_id": "5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40"}) ``` ```js Node.js theme={null} await call("/v1/payment/link/info", { link_id: "5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40" }); ``` ### Пример ответа Та же карточка ссылки, что в `/list`, плюс массив `payments` — все счета, порождённые этой ссылкой: ```json theme={null} { "state": 0, "result": { "link_id": "5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40", "title": "Поддержать проект", "amount_mode": "open", "currency": "USD", "active": true, "url": "https://pay.oblodai.com/link/5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40", "created_at": "2026-07-13T12:00:00Z", "payments": [ { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "status": "paid", "amount": "10.00", "currency": "USD", "created_at": "2026-07-13T12:31:00Z" } ] } } ``` Полный объект каждого платежа берите через [`POST /v1/payment/info`](/reference/payment-info) по его `uuid`. *** ## POST /v1/payment/link/toggle — включить / выключить Выключенная ссылка перестаёт открываться: публичные методы отдают `404 paylink.not_found`, новые счета по ней не создаются. Уже созданные счета продолжают жить и оплачиваться. ### Параметры запроса Идентификатор ссылки. `false` — выключить, `true` — включить обратно. ```python Python theme={null} call("/v1/payment/link/toggle", {"link_id": "5d3f2a71-…", "active": False}) ``` ```js Node.js theme={null} await call("/v1/payment/link/toggle", { link_id: "5d3f2a71-…", active: false }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "link_id": "5d3f2a71-9c84-4b0e-8d17-3e6a2c9f1b40", "active": false } } ``` *** ## Коды ошибок | Код | Значение | | ------------------------------ | ------------------------------------------------------ | | `400 request.bad_json` | Тело не парсится. | | `400 paylink.bad_id` | `link_id` не является корректным UUID. | | `404 paylink.not_found` | Ссылка не найдена (или принадлежит другому мерчанту). | | `400 paylink.bad_mode` | `amount_mode` не `fixed` / `open` / `range`. | | `400 paylink.unknown_currency` | Неизвестная `currency`. | | `400 paylink.bad_amount` | `amount_fixed` не положительное число (режим `fixed`). | | `400 paylink.bad_min` | `amount_min` не положительное число. | | `400 paylink.bad_max` | `amount_max` не положительное число. | | `400 paylink.bad_range` | `amount_min` больше `amount_max`. | | `503 paylink.disabled` | Платёжные ссылки недоступны на этом шлюзе. | | `401 auth.*` | Ошибки аутентификации. | Ошибки, которые возникают при оплате **покупателем** (`paylink.amount_required`, `paylink.below_min`, `paylink.above_max`, `paylink.not_positive`), — на странице [публичных методов](/reference/link-public). *** ## Нюансы * **Каждый checkout — новый счёт.** Ссылка — не счёт, а «фабрика счетов»: каждый checkout создаёт новый инвойс со своим адресом и своим сроком жизни. * **Комиссия, скидки, минимумы, допуски — как у обычного счёта.** Платёж по ссылке проходит ровно тот же путь, что [`POST /v1/payment`](/reference/payment-create), и точно так же присылает [вебхуки](/reference/webhook-object). * **Цена в фиате работает.** `currency` может быть любым из 23 поддерживаемых фиатов, а не только `USD` — см. [Форматы сумм и денег](/reference/basics-money). * **`order_id` у платежей по ссылке нет** — вы его не задаёте (покупатель приходит без вашего контекста). Сопоставляйте платежи со ссылкой через `/link/info` — по `uuid` счёта из [вебхука](/reference/webhook-object). *** ## Связанные страницы публичная страница оплаты. какие валюты цены и какие монеты доступны. # Объект платежа (Payment) Source: https://docs.oblodai.com/reference/payment-object Объект платежа (инвойса) содержит всю информацию о счёте, актуальную на текущий момент. Он формируется при создании платежа ([`POST /v1/payment`](/reference/payment-create)) и приходит в ответ на любой запрос, связанный с этим счётом ([`/info`](/reference/payment-info), [`/history`](/reference/payment-history)). Объект может содержать поля, не описанные в этом справочнике, — их следует игнорировать. *** ## Пример объекта ```json theme={null} { "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "order_id": "order-1", "amount": "10.00", "payment_amount": null, "amount_paid": "0", "amount_remaining": "10.150000", "payer_amount": "10.150000", "payer_currency": "USDT", "payer_address": "", "currency": "USD", "network": "tron", "address": "TJ4b1...C9xk", "address_qr_code": "data:image/png;base64,iVBORw0KGgo...", "payment_status": "check", "is_multi": false, "url": "https://pay.oblodai.com/pay/8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e", "expired_at": 1783728000, "is_final": false, "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:00:00Z", "additional_data": "", "payer_email": "", "url_return": "", "url_success": "", "rate_expires_at": 1783724700, "confirmations": 0, "required_confirmations": 20, "txid": "", "refund_status": "none", "refunds": [] } ``` Поля `refunds[]` и `refund_status` приходят только в ответе [`POST /v1/payment/info`](/reference/payment-info) — в списках ([`/history`](/reference/payment-history)) их нет. *** ## Поля `currency` — **валюта цены**, в которой вы назначили стоимость счёта (напр. `USD`). `payer_currency` — **валюта расчёта**, крипта, которой фактически платит покупатель (напр. `USDT`). Поэтому в объекте две валюты и две суммы. См. [Форматы сумм и денег](/reference/basics-money). Идентификатор счёта в Oblodai. Ваша ссылка на заказ; ключ идемпотентности. Сумма к оплате в валюте ценообразования `currency`. Фактически оплаченная сумма (в крипте); `null`, пока оплаты нет. Сколько уже поступило. Сколько осталось доплатить в крипте. Ожидаемая сумма к оплате в крипте `payer_currency`. Криптовалюта, которой платит покупатель (валюта расчёта). У валюто‑агностичного счёта (`is_multi: true`) — **пустая строка**, пока покупатель не выбрал монету. **Адрес, с которого пришли деньги.** Это адрес возврата по умолчанию: [`/v1/payment/refund`](/reference/payment-refund) без `address` вернёт средства сюда, и такой возврат авто‑подтверждается. Известен на аккаунтных сетях (EVM, Tron, Solana, TON); на **Bitcoin/UTXO пуст** — там единого отправителя нет. Пуст и до оплаты. Валюта цены (напр. `USD`, `EUR`, `USDT`). Сеть расчёта. Депозит‑адрес счёта. Для агностичного счёта пуст до выбора валюты. QR депозит‑адреса как `data:`‑URI. Пуст, пока адреса нет. Статус счёта. См. [таблицу ниже](#статусы-payment_status). `true` — валюто‑агностичный (deferred) счёт. Ссылка на hosted‑страницу оплаты. Момент истечения счёта (unix‑секунды). Счёт в терминальном статусе (изменений больше не будет). Время создания (ISO 8601). Время последнего изменения (ISO 8601). Приватные данные мерчанта, эхом в вебхуках (покупателю не видны). Email плательщика, если передавали. Ссылка «назад в магазин» на странице оплаты. Редирект после успешной оплаты. До какого момента действителен зафиксированный курс (unix‑секунды). Набрано подтверждений сети. Сколько подтверждений нужно для зачёта (зависит от суммы/сети). Хеш транзакции оплаты; пуст до появления транзакции. Сводка по возвратам: `none` (ничего не возвращали) | `partial` (вернули часть) | `full` (вернули всё оплаченное). Только в [`/v1/payment/info`](/reference/payment-info). Список возвратов по этому счёту (см. ниже). Только в [`/v1/payment/info`](/reference/payment-info). ### Элемент `refunds[]` Идентификатор возврата (это выплата — см. [объект выплаты](/reference/payout-object)). Укрупнённый статус выплаты: `check` | `process` | `paid` | `fail` | `cancel`. Сумма возврата в монете платежа. Куда ушёл возврат. Хеш транзакции возврата; пуст, пока не отправлен. Возврат в терминальном статусе. Время создания (ISO 8601). Отменённые и провалившиеся возвраты (`cancel`, `fail`) **не считаются** в `refund_status` — денег они не вернули. Для валюто‑агностичного счёта (`is_multi: true`) поля `payer_currency`, `address`, `address_qr_code`, `payer_amount`, `amount_paid`, `amount_remaining` пусты до выбора монеты покупателем на [hosted‑странице](/reference/pay-select): валюты расчёта у такого счёта ещё нет, а значит нет и сумм в ней. `currency` и `amount` (цена) при этом заполнены всегда. *** ## Статусы `payment_status` | Статус | Значение | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `select` | Валюто‑агностичный счёт (`is_multi: true`): ждём, пока покупатель выберет валюту и сеть на [hosted‑странице](/reference/pay-select). До выбора `address`, `payer_amount` и т.п. пусты. | | `check` | Счёт создан, ждём оплату. | | `confirm_check` | Транзакцию видим, ждём подтверждений сети. | | `wrong_amount_waiting` | Пришла недоплата, ждём остаток (срок ещё не вышел). | | `paid` | Оплачено в пределах допуска. | | `paid_over` | Переплата сверх допуска. | | `wrong_amount` | Недоплата, срок вышел. | | `cancel` | Счёт истёк или отменён. | **Терминальные статусы** (`is_final: true`): `paid`, `paid_over`, `wrong_amount`, а также истёкшие/отменённые счета. Недоплата и переплата обрабатываются согласно вашим настройкам [`accuracy`](/reference/payment-accuracy) (допуск) и [`autorefund`](/reference/payment-autorefund) (автовозврат). *** ## Связанные страницы создание счёта. получить актуальный объект (с `refunds[]`). возврат; по умолчанию идёт на `payer_address`. как это выглядит в потоке приёма. # POST /v1/payment/qr Source: https://docs.oblodai.com/reference/payment-qr Рендерит депозит‑адрес счёта (по `uuid` или `order_id`) в QR‑код — PNG в виде `data:`‑URI. **URL:** `https://api.oblodai.com/v1/payment/qr` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Нужен хотя бы один идентификатор. Идентификатор счёта. Ваша ссылка на заказ. *** ## Пример запроса ```bash cURL theme={null} BODY='{"order_id":"order-1"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/qr' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/qr \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/qr", {"order_id": "order-1"}) ``` ```js Node.js theme={null} await call("/v1/payment/qr", { order_id: "order-1" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "image": "data:image/png;base64,…" } } ``` *** ## Коды ошибок | Код | Значение | | ----------------------- | ------------------------------------ | | `400 request.bad_json` | Тело не парсится. | | `400 payment.bad_uuid` | Некорректный `uuid`. | | `400 payment.no_lookup` | Не передан ни `uuid`, ни `order_id`. | | `404 payment.not_found` | Счёт не найден. | *** ## Нюансы * **Для валюто‑агностичного счёта вызывайте этот метод только после того, как плательщик выбрал валюту и сеть** (см. [`POST /v1/pay/{id}/select`](/reference/pay-select)). До выбора реальный адрес ещё не выделен, и `image` закодирует служебный плейсхолдер, а не платёжный адрес — такой QR показывать покупателю нельзя. * Чтобы отрисовать произвольный адрес (не привязанный к счёту), используйте [`POST /v1/wallet/qr`](/reference/wallet-qr). *** ## Связанные страницы # POST /v1/payment/refund Source: https://docs.oblodai.com/reference/payment-refund Вернуть средства платежа на адрес. Использует движок выплат (списание с баланса). **URL:** `https://api.oblodai.com/v1/payment/refund` · **Аутентификация:** обязательна · **Идемпотентность:** заголовок `Idempotency-Key` или тройка `(платёж, адрес, сумма)`. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Идентификатор платежа. Ваша ссылка на заказ платежа. Адрес назначения возврата. **По умолчанию — `payer_address`**, адрес, с которого пришли деньги. Сеть. Частичная сумма. По умолчанию — вся полученная. **Минимально необходимое:** `uuid` или `order_id`. Всё остальное — по мере надобности. **`address` необязателен.** Не передавайте его — и возврат уйдёт на [`payer_address`](/reference/payment-object) платежа. Обязателен он только для **Bitcoin/UTXO**: у таких платежей нет единого отправителя, `payer_address` пуст, и без явного адреса вы получите `400 refund.no_address`. ### Заголовки Любое уникальное значение (≤255 символов), одинаковое во всех повторах одного возврата. Повтор вернёт тот же ответ и заголовок `Idempotent-Replayed: true`. → [Идемпотентность](/reference/basics-idempotency) *** ## Пример запроса **Обычный случай — вернуть всё плательщику.** Ни адреса, ни суммы указывать не надо: ```bash cURL theme={null} BODY='{"order_id":"order-1"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/refund' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/refund \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -H "Idempotency-Key: refund-order-1" \ -d "$BODY" ``` ```python Python theme={null} # Вернуть всё, что заплатили, на адрес плательщика: call("/v1/payment/refund", {"order_id": "order-1"}) # Частичный возврат — тоже на адрес плательщика: call("/v1/payment/refund", {"order_id": "order-1", "amount": "10"}) # На явный адрес — нужно для Bitcoin/UTXO, либо когда возврат идёт не отправителю: call("/v1/payment/refund", {"order_id": "order-1", "address": "TZZ...", "network": "tron"}) ``` ```js Node.js theme={null} await call("/v1/payment/refund", { order_id: "order-1" }); await call("/v1/payment/refund", { order_id: "order-1", amount: "10" }); await call("/v1/payment/refund", { order_id: "order-1", address: "TZZ...", network: "tron" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "uuid": "f9e8...01", "payment_uuid": "a1b2...c3", "order_id": "order-1", "amount": "10", "currency": "USDT", "address": "TZZ...", "status": "process", "is_final": false } } ``` *** ## Коды ошибок | Код | Значение | | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400 refund.no_address` | Адрес не передан **и** не известен адрес плательщика (Bitcoin/UTXO). Передайте `address` явно. | | `400 refund.bad_amount` | Некорректная сумма. | | `400 refund.nothing_to_refund` | Нечего возвращать. В том числе — если это валюто‑агностичный счёт (`is_multi: true`), где покупатель ещё не выбрал монету: валюты расчёта нет, значит платежа не было. | | `400 refund.dust` | Сумма возврата ниже пыле‑порога сети. | | `400 refund.destination_internal` | Адрес назначения принадлежит шлюзу. | | `400 refund.exceeds_refundable` | Больше оплаченной суммы вернуть нельзя. | | `400 refund.exceeds_excess` | Возврат переплаты превышает саму переплату. | | `409 refund.reference_collision` | Тот же `(платёж, адрес, сумма)` уже в работе (идемпотентность). | | `400 payment.bad_uuid` | Некорректный `uuid`. | | `400 payment.no_lookup` | Не передан ни `uuid`, ни `order_id`. | | `404 payment.not_found` | Платёж не найден. | *** ## Нюансы * **Адрес по умолчанию — адрес плательщика.** Шлюз запоминает, откуда пришли деньги ([`payer_address`](/reference/payment-object)), и без явного `address` возвращает туда же. На аккаунтных сетях (EVM, Tron, Solana, TON) отправитель известен всегда. На **Bitcoin/UTXO** единого отправителя нет — там `address` обязателен. * **Возврат по API‑ключу авто‑одобряется** и уходит сразу — как на адрес плательщика, так и на любой другой (отдельное подтверждение через [`/v1/payout/approve`](/reference/payout-approve) не требуется, как и для обычной выплаты). Поэтому не передавайте `address` без нужды и перепроверяйте его, если передаёте: отправленный возврат не остановить. * **Валюта возврата — та, что фактически пришла.** Возврат деноминирован в монете платежа и **не пересчитывается** по новому курсу. Если счёт был в `USD`, а заплатили `USDT`, вернётся `USDT` — ровно столько, сколько пришло. Курсовую разницу шлюз не компенсирует. * **Идемпотентность.** Заголовок `Idempotency-Key` либо тройка `(платёж, адрес, сумма)`. Суммарно **нельзя вернуть больше оплаченного**. * Кто несёт **нашу** комиссию при возврате — настраивается через [`refund-fee-config`](/reference/payout-refund-fee-config). * Автоматический (не ручной) возврат недоплаты/переплаты настраивается отдельно — [`autorefund`](/reference/payment-autorefund). * **Много возвратов сразу** — [`POST /v1/refund/batch`](/reference/refund-batch) (до 5000 за один запрос). * Что уже возвращено по счёту, видно в полях `refunds[]` и `refund_status` ответа [`POST /v1/payment/info`](/reference/payment-info). *** ## Связанные страницы массовые возвраты. `payer_address`, `refunds[]`, `refund_status`. # POST /v1/payment/resend Source: https://docs.oblodai.com/reference/payment-resend Повторно ставит в очередь текущий вебхук платежа (по `uuid` или `order_id`). Событие — `invoice.<статус>`. **URL:** `https://api.oblodai.com/v1/payment/resend` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Нужен хотя бы один идентификатор. Идентификатор платежа. Ваша ссылка на заказ. *** ## Пример запроса ```bash cURL theme={null} BODY='{"order_id":"order-1001"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/resend' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/resend \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payment/resend", {"order_id": "order-1001"}) ``` ```js Node.js theme={null} await call("/v1/payment/resend", { order_id: "order-1001" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "result": true } } ``` *** ## Коды ошибок | Код | Значение | | ----------------------- | ------------------------------------ | | `400 payment.no_lookup` | Не передан ни `uuid`, ни `order_id`. | | `400 payment.bad_uuid` | Некорректный `uuid`. | | `404 payment.not_found` | Платёж не найден. | *** ## Нюансы * Переотправка **создаёт ещё одну доставку** того же события. Ваш обработчик должен быть идемпотентен (дедуп по `uuid` + `status`) — см. [Объект вебхука](/reference/webhook-object). * Событие соответствует **текущему** статусу платежа: `invoice.<статус>`. *** ## Связанные страницы # POST /v1/payment/resolve Source: https://docs.oblodai.com/reference/payment-resolve Решите судьбу недоплаченного счёта: принять частичную оплату или вернуть её плательщику. Решение судьбы **недоплаченного** платежа (статус `wrong_amount`): `accept` — оставить частичную оплату себе (и отключить [автовозврат](/reference/payment-autorefund) для этого счёта) или `refund` — вернуть средства плательщику. Средства недоплаты к этому моменту уже зачислены на ваш баланс, вебхук `invoice.wrong_amount` уже отправлен. **URL:** `https://api.oblodai.com/v1/payment/resolve` · **Аутентификация:** обязательна, **payout-ключ** · **Идемпотентность:** заголовок `Idempotency-Key` поддержан; решение дополнительно идемпотентно на уровне счёта (одно на счёт). Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). ## Параметры запроса UUID платежа. Нужен `uuid` **или** `order_id`. Ваш идентификатор платежа. `accept` — принять частичную оплату · `refund` — вернуть плательщику. Только для `refund`: адрес возврата. По умолчанию — записанный `payer_address` платежа; если он пуст (Bitcoin/UTXO), адрес обязателен, иначе `refund.no_address`. Только для `refund`: сеть возврата, по умолчанию — сеть платежа. Только для `refund`: ваш ключ дедупликации возврата. ## Пример запроса ```python Python theme={null} # принять недоплату (оставить частичную оплату себе) call("/v1/payment/resolve", {"order_id": "ord-1001", "action": "accept"}) # либо вернуть плательщику call("/v1/payment/resolve", {"order_id": "ord-1001", "action": "refund"}) ``` ```js Node.js theme={null} // принять недоплату (оставить частичную оплату себе) await call("/v1/payment/resolve", { order_id: "ord-1001", action: "accept" }); // либо вернуть плательщику await call("/v1/payment/resolve", { order_id: "ord-1001", action: "refund" }); ``` ## Пример ответа ```json accept theme={null} { "state": 0, "result": { "payment_uuid": "e0a1…", "order_id": "ord-1001", "resolution": "accepted", "amount_kept": "48.5", "currency": "USDT" } } ``` ```json refund theme={null} { "state": 0, "result": { "payment_uuid": "e0a1…", "order_id": "ord-1001", "resolution": "refunded", "uuid": "f2b3…", "amount": "48.5", "currency": "USDT", "address": "0xPayer…", "status": "check", "is_final": false } } ``` При `refund` ответ содержит обычный объект возврата-выплаты (`uuid`, `status` из словаря выплат) — отслеживайте его как [выплату](/reference/payout-info). ## Коды ошибок `action` не `accept` и не `refund`. **Повтор:** Нет. Платёж не в статусе `wrong_amount` (например, поздняя доплата перевела его в `paid`). **Повтор:** Нет — сверьтесь с `/v1/payment/info`. Решение по счёту уже принято (противоположное действие). **Повтор:** Нет. По счёту уже был возврат — принять недоплату нельзя. **Повтор:** Нет. Не указан адрес возврата, а `payer_address` у платежа пуст (UTXO-сети). **Повтор:** Нет — передайте `address`. Платёж не найден. **Повтор:** Нет — сверьте `uuid`/`order_id`. Функция выключена на шлюзе. **Повтор:** Нет — обратитесь в поддержку. Возврат также может вернуть обычные ошибки возвратов: `refund.nothing_to_refund`, `refund.dust`, `refund.destination_internal`, `compliance.blocked` и др. — см. [Возврат платежа](/reference/payment-refund). ## Нюансы * **`accept` останавливает автовозврат** для этого счёта: решение и поллер автовозврата сериализованы на локе счёта — гонки «приняли и одновременно вернули» нет. * Повторный `accept` — no-op (идемпотентно); `refund` после `accept` (и наоборот) → `resolution.already_resolved`. * `accept` не эмитит вебхуков; `refund` порождает выплату с обычными [`payout.*`](/reference/webhook-object) событиями. ## Связанные страницы # POST /v1/payment/send-email Source: https://docs.oblodai.com/reference/payment-send-email Отправить покупателю **письмо со счётом** — с суммой и кнопкой «Оплатить», ведущей на [hosted‑страницу оплаты](/reference/pay-get). Полезно, когда счёт выставлен, а покупателя нужно к нему привести: выставили инвойс по почте, напомнили о неоплаченном заказе. **URL:** `https://api.oblodai.com/v1/payment/send-email` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Чек уходит сам **Если при создании платежа задан `payer_email`, на этот адрес автоматически уходит ЧЕК после оплаты.** Ничего вызывать для этого не нужно — достаточно передать `payer_email` в [`POST /v1/payment`](/reference/payment-create) (или в [`POST /v1/link/{id}/checkout`](/reference/link-public), где его вводит сам покупатель). `POST /v1/payment/send-email` — про **другое**: это письмо **до** оплаты, приглашение заплатить. Его вы шлёте сами и можете слать повторно. | | Письмо со счётом («Оплатите») | Чек («Оплачено») | | --------------- | ----------------------------------------------- | -------------------------- | | Когда | Когда вы вызовете `/v1/payment/send-email` | Автоматически после оплаты | | Кому | `email` из запроса, иначе `payer_email` платежа | `payer_email` платежа | | Нужен вызов API | Да | **Нет** | *** ## Параметры запроса Идентификатор платежа в Oblodai. Ваша ссылка на заказ. Кому отправить. По умолчанию — `payer_email`, заданный у платежа. Нужен `uuid` **или** `order_id`. Если `email` не передан **и** у платежа нет `payer_email` — слать некуда, придёт `400 email.no_recipient`. *** ## Пример запроса ```bash cURL theme={null} BODY='{"order_id":"order-1"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/send-email' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/send-email \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} # На payer_email, заданный при создании платежа: call("/v1/payment/send-email", {"order_id": "order-1"}) # На другой адрес: call("/v1/payment/send-email", {"uuid": "8b1d7d2e-…", "email": "buyer@example.com"}) ``` ```js Node.js theme={null} await call("/v1/payment/send-email", { order_id: "order-1" }); await call("/v1/payment/send-email", { uuid: "8b1d7d2e-…", email: "buyer@example.com" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "sent": true, "email": "buyer@example.com", "uuid": "8b1d7d2e-2b0a-4a1f-9c3e-1f2a3b4c5d6e" } } ``` Письмо принято в очередь отправки. Фактический получатель. Платёж, по которому отправлено письмо. *** ## Коды ошибок | Код | Значение | | ------------------------ | -------------------------------------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 email.no_recipient` | Получатель не определён: не передан `email` и у платежа нет `payer_email`. | | `503 email.disabled` | Почта не настроена на этом шлюзе. | | `400 payment.bad_uuid` | Некорректный `uuid`. | | `400 payment.no_lookup` | Не передан ни `uuid`, ни `order_id`. | | `404 payment.not_found` | Платёж не найден. | | `401 auth.*` | Ошибки аутентификации. | *** ## Нюансы * **Повторная отправка разрешена.** Дедупликации нет: вызвав метод дважды, вы отправите два письма. Это сделано намеренно — «напомнить об оплате» законная операция. * **`sent: true` — это «принято в очередь»**, а не «доставлено в почтовый ящик». Доставку писем Oblodai не гарантирует и статус не отдаёт. * **Истёкший счёт не блокирует отправку.** По истёкшему счёту письмо отправится, но кнопка «Оплатить» приведёт на страницу просроченного счёта. Перед отправкой напоминания проверяйте статус через [`POST /v1/payment/info`](/reference/payment-info) — или оживите счёт через `is_refresh` у [`POST /v1/payment`](/reference/payment-create). *** ## Связанные страницы где задаётся `payer_email` (он же включает автоматический чек). покупатель вводит email сам. # POST /v1/payment/services Source: https://docs.oblodai.com/reference/payment-services Список доступных методов приёма (пар валюта + сеть) с лимитами, комиссией и флагом доступности. **URL:** `https://api.oblodai.com/v1/payment/services` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Тело:** `{}`. Используйте этот метод, чтобы **программно** узнать актуальный набор методов приёма, а не полагаться на статичную [таблицу сетей](/reference/basics-networks). *** ## Пример запроса ```bash cURL theme={null} TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payment/services' '{}' \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payment/services \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d '{}' ``` ```python Python theme={null} call("/v1/payment/services", {}) ``` ```js Node.js theme={null} await call("/v1/payment/services", {}); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": [ { "network": "tron", "currency": "USDT", "is_available": true, "limit": { "min_amount": "0", "max_amount": "" }, "commission": { "fee_amount": "0", "percent": "0" } } ] } ``` Код сети. Код валюты. Есть живой watcher метода и сеть сконфигурирована. Минимальная сумма (`"0"` = без нижнего лимита). Максимум (пусто = без верхнего лимита). Фиксированная часть комиссии. Процентная часть комиссии. *** ## Нюансы * `is_available` = есть живой watcher метода **и** сеть сконфигурирована. Ориентируйтесь на этот флаг перед созданием счёта. * `limit`/`commission` пока не настраиваются per‑method и возвращают дефолтную форму (`min_amount: "0"`, пустой `max_amount` = без верхнего лимита). *** ## Связанные страницы аналог для выплат. # POST /v1/payout/approve Source: https://docs.oblodai.com/reference/payout-approve Подтвердить выплату в статусе `pending` (`check`). **Для выплат по API‑ключу не требуется.** Такие выплаты авто‑одобряются. Этот метод нужен только внутренним/кабинетным выплатам, оставшимся в `pending` (создаваемым без авто‑одобрения). **URL:** `https://api.oblodai.com/v1/payout/approve` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Идентификатор выплаты. *** ## Пример запроса ```bash cURL theme={null} BODY='{"uuid":"0c1f...e9"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/approve' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/approve \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/approve", {"uuid": "0c1f...e9"}) ``` ```js Node.js theme={null} await call("/v1/payout/approve", { uuid: "0c1f...e9" }); ``` *** ## Пример ответа Возвращается **обновлённый объект выплаты** — тот же формат, что у [`POST /v1/payout/info`](/reference/payout-info). После успешного подтверждения внутренний статус становится `approved` (укрупнённый `process`), `approval_required` сбрасывается в `false`, и выплата уходит в обработку. Полное описание полей — [Объект выплаты](/reference/payout-object). ```json theme={null} { "state": 0, "result": { "uuid": "0c1f...e9", "order_id": "payout-1", "amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "txid": "", "status": "process", "is_final": false, "approval_required": false, "source": "manual", "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:02:00Z" } } ``` ## Коды ошибок | Код | Значение | | -------------------------------- | ------------------------------------------------------ | | `400 payout.bad_uuid` | Некорректный `uuid`. | | `404 payout.not_found` | Выплата не найдена. | | `409 payout.not_pending` | Выплата не в статусе `pending`. | | `409 payout.frozen` | Выплаты заморожены (сработал kill‑switch). | | `403 payout.approver_is_creator` | Подтверждающий совпадает с создателем (maker‑checker). | *** ## Нюансы * Действует принцип **maker‑checker**: подтвердить выплату должен не тот, кто её создал (`payout.approver_is_creator`). Для API‑ключевых выплат подтверждение не нужно вовсе. *** ## Связанные страницы # POST /v1/payout/batch Source: https://docs.oblodai.com/reference/payout-batch Массовая выплата — до **5000** выплат одним запросом. Обрабатывается асинхронно: в ответ сразу приходит `batch_id`, результат по каждому элементу забирается через [`POST /v1/batch/info`](/reference/batch-info). **Это штатный способ не упираться в rate limit** и основной путь для больших пачек выплат. Один подписанный запрос вместо 5000. Устаревший [`POST /v1/payout/mass`](/reference/payout-mass) (до 100 элементов, синхронный) остаётся для маленьких пачек, но для сотен и тысяч выплат используйте батч. **URL:** `https://api.oblodai.com/v1/payout/batch` · **Аутентификация:** обязательна · **Идемпотентность:** заголовок `Idempotency-Key` (на весь батч) + `order_id` каждого элемента (обязателен). Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Массив от 1 до **5000** элементов. Поля каждого элемента — **ровно те же**, что у [`POST /v1/payout`](/reference/payout-create). Что делать при ошибке элемента: `continue` (по умолчанию) — обрабатывать остальные; `stop` — прекратить обработку после первой ошибки. У каждого элемента `order_id` **обязателен** — как и у одиночной выплаты. Он же ключ идемпотентности: повторная отправка того же элемента вернёт уже созданную выплату, а не вторую. *** ## Пример запроса ```bash cURL theme={null} BODY='{"payouts":[ {"amount":"25","currency":"USDT","network":"tron","address":"TXY...","order_id":"p-1"}, {"amount":"10","currency":"USDT","network":"tron","address":"TZZ...","order_id":"p-2"} ],"on_error":"continue"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/batch' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/batch \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -H "Idempotency-Key: payroll-2026-07" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/batch", { "payouts": [ {"amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "p-1"}, {"amount": "10", "currency": "USDT", "network": "tron", "address": "TZZ...", "order_id": "p-2"}, ], "on_error": "continue", }) ``` ```js Node.js theme={null} await call("/v1/payout/batch", { payouts: [ { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "p-1" }, { amount: "10", currency: "USDT", network: "tron", address: "TZZ...", order_id: "p-2" }, ], on_error: "continue", }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "batch_id": "b71e0d3f-8a94-4c02-95a7-2f6c8d1e4b55", "kind": "payout", "count": 2, "status": "pending" } } ``` Идентификатор батча — с ним в [`POST /v1/batch/info`](/reference/batch-info). Вид батча — здесь всегда `payout`. Сколько элементов принято в обработку. Стартовый статус — всегда `pending`. **Выплат в ответе нет** — только подтверждение приёма. `uuid` каждой выплаты появится в `items[].result` ответа [`/v1/batch/info`](/reference/batch-info). *** ## Батч vs `/v1/payout/mass` | | [`/v1/payout/batch`](/reference/payout-batch) | [`/v1/payout/mass`](/reference/payout-mass) | | ------------------ | --------------------------------------------- | --------------------------------------------- | | Максимум элементов | **5000** | 100 | | Обработка | асинхронная, `batch_id` + поллинг | синхронная, результаты сразу в ответе | | Когда использовать | **основной путь**, любые объёмы | маленькая пачка, когда нужен ответ немедленно | *** ## Коды ошибок | Код | Значение | | ---------------------- | -------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 batch.empty` | Массив `payouts` пуст. | | `400 batch.too_large` | Больше 5000 элементов. | | `503 batch.disabled` | Массовая обработка недоступна на этом шлюзе. | | `401 auth.*` | Ошибки аутентификации. | Ошибки отдельных выплат (`payout.insufficient_funds`, `payout.bad_address`, `payout.order_id_required` и т. п.) приходят в `items[].error` ответа [`/v1/batch/info`](/reference/batch-info). *** ## Нюансы * **Баланса может не хватить на весь батч.** Элементы списывают баланс по очереди; когда средства кончатся, остальные упадут с `payout.insufficient_funds` (при `on_error: "continue"`). Если хотите, чтобы обработка остановилась на первой такой ошибке, — `on_error: "stop"`. * **`payout.funds_maturing` — ретраибельная ошибка.** Средства ещё дозревают; такой элемент можно отправить повторно позже (`order_id` не даст создать дубль). * **Выплаты по API‑ключу авто‑одобряются** — батч этого не меняет. *** ## Связанные страницы поллинг статуса и результатов. легаси‑путь на ≤100 элементов. # POST /v1/payout/calculate Source: https://docs.oblodai.com/reference/payout-calculate Предрасчёт выплаты — комиссия и итоговые суммы **без** создания выплаты. **URL:** `https://api.oblodai.com/v1/payout/calculate` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). Полезно, чтобы показать пользователю итоговую сумму до подтверждения или проверить, покрывает ли баланс выплату с учётом комиссии. *** ## Параметры запроса Сумма выплаты в `currency`. Код валюты. Сеть (обязательна для монет с несколькими сетями). `true`: комиссия **сверх** суммы (списывается с баланса). `false`: комиссия **из** суммы (получатель получает меньше). *** ## Пример запроса ```bash cURL theme={null} BODY='{"amount":"25","currency":"USDT","network":"tron","is_subtract":false}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/calculate' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/calculate \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/calculate", {"amount": "25", "currency": "USDT", "network": "tron", "is_subtract": False}) ``` ```js Node.js theme={null} await call("/v1/payout/calculate", { amount: "25", currency: "USDT", network: "tron", is_subtract: false }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "amount": "25", "currency": "USDT", "network": "tron", "commission": "1.1", "merchant_amount": "25", "to_amount": "23.9" } } ``` **Сетевая** комиссия (газ сети). Это **не** комиссия платформы Oblodai — платформенная удерживается при приёме платежа, а не здесь. Сколько уйдёт с баланса. Сколько получит адрес назначения. *** ## Коды ошибок | Код | Значение | | ----------------------------- | ------------------------------ | | `400 payout.unknown_currency` | Неизвестная валюта. | | `400 payout.bad_amount` | Некорректная сумма. | | `400 payout.amount_below_fee` | Сумма меньше сетевой комиссии. | *** ## Связанные страницы # POST /v1/payout Source: https://docs.oblodai.com/reference/payout-create Создать выплату на внешний адрес. Списывает баланс на `amount`; получателю приходит `amount` минус сетевая комиссия (если она переложена на получателя — см. [`fee-config`](/reference/payout-fee-config)). **URL:** `https://api.oblodai.com/v1/payout` · **Аутентификация:** обязательна · **Идемпотентность:** заголовок `Idempotency-Key` и/или `order_id` (для выплат `order_id` обязателен). Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Выплаты по API‑ключу авто‑одобряются** и уходят сразу — без белых списков и периодов выдержки. Ответственность за адрес назначения на вашей стороне. ### Заголовки Любое уникальное значение (≤255 символов), одинаковое во всех повторах одной выплаты. Повтор вернёт тот же ответ и заголовок `Idempotent-Replayed: true`. Работает вместе с `order_id`. → [Идемпотентность](/reference/basics-idempotency) *** ## Параметры запроса Сумма выплаты в `currency`. Код валюты (например `USDT`). Ваш номер выплаты; **ключ идемпотентности**. Адрес получателя. Сеть (`tron`, `ethereum`, …). Обязательна для монет с несколькими сетями. Кто платит сетевую комиссию. `true` — комиссия **сверх** суммы: с баланса списывается `amount + fee`, получатель получает ровно `amount`. `false` — комиссия **из** суммы: списывается `amount`, получатель получает `amount − fee`. Не передано — действует [fee-config](/reference/payout-fee-config) проекта (иначе дефолт шлюза). Семантика совпадает с предрасчётом `/v1/payout/calculate`. Тег/мемо назначения (TON Jetton). Максимум 120 символов. Свой URL вебхука для этой выплаты (SSRF‑проверка). Профинансировать выплату конвертацией баланса. Поддерживается только `USDT` → `currency`. Метка происхождения: `api` (по умолчанию) или `manual`. **По умолчанию сетевую комиссию выплаты несёт получатель.** Кто её платит, определяется настройкой проекта [fee-config](/reference/payout-fee-config) (общий дефолт — комиссию несёт получатель), а **не** полем запроса. **`is_subtract` учитывается и выплатой, и предрасчётом.** Переданный в `/v1/payout` `is_subtract` применяется ровно так же, как его моделирует [`/v1/payout/calculate`](/reference/payout-calculate), поэтому превью совпадает с реальной выплатой при одинаковом значении поля. Если поле не передавать, плательщик комиссии берётся из [fee‑config](/reference/payout-fee-config) проекта. *** ## Пример запроса ```bash cURL wrap theme={null} BODY='{"amount":"25","currency":"USDT","network":"tron","address":"TXY...","order_id":"payout-1"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout", { "amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "payout-1", }) ``` ```js Node.js theme={null} await call("/v1/payout", { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "payout-1", }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "uuid": "0c1f...e9", "order_id": "payout-1", "amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "txid": "", "status": "process", "is_final": false, "approval_required": false, "source": "api", "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:00:00Z" } } ``` При `from_currency` в `result` добавляется объект `convert`: ```json theme={null} { "from_currency": "USDT", "to_currency": "TRX", "from_amount": "40.12", "rate": "0.062" } ``` Полное описание полей и статусов — [Объект выплаты](/reference/payout-object). *** ## Коды ошибок | Код | Значение | | -------------------------------------- | ------------------------------------------ | | `400 payout.unknown_currency` | Неизвестная валюта. | | `400 payout.bad_amount` | Некорректная сумма. | | `400 payout.order_id_required` | Не передан `order_id`. | | `400 payout.destination_internal` | Адрес принадлежит шлюзу (self‑dealing). | | `400 payout.from_currency_unsupported` | Неподдерживаемая конвертация. | | `400 payout.memo_too_long` | `memo` длиннее 120 символов. | | `400 payout.bad_url_callback` | Некорректный `url_callback` (SSRF). | | `400 payout.no_destination` | Не передан адрес назначения. | | `400 payout.asset_mismatch` | Валюта/сеть не соответствуют друг другу. | | `400/403` | Валидация адреса под сеть / комплаенс. | | `409 payout.frozen` | Выплаты заморожены (сработал kill‑switch). | | `409 payout.insufficient_funds` | Недостаточно доступного баланса. | | `409 payout.funds_maturing` | Средства ещё дозревают (maturity‑холд). | *** ## Нюансы * **Идемпотентность по `(мерчант, order_id)`.** Повтор вернёт уже созданную выплату. * **Выплата на адрес самого шлюза запрещена** (`payout.destination_internal`). * **`from_currency`** конвертирует баланс до резервирования и идемпотентна по `order_id`. Поддерживается только `USDT` → `currency`. * Выплаты по API‑ключу авто‑одобряются (`approval_required: false`) — шаг [`/approve`](/reference/payout-approve) не нужен. * Перед созданием можно оценить комиссию и итоговые суммы через [`POST /v1/payout/calculate`](/reference/payout-calculate). * **Много выплат?** → [`POST /v1/payout/batch`](/reference/payout-batch) — до 5000 одним запросом. *** ## Связанные страницы Много выплат сразу — до 5000 одним запросом. # Сетевая комиссия выплаты Source: https://docs.oblodai.com/reference/payout-fee-config **Методы:** `/v1/payout/fee-config/get` · `/v1/payout/fee-config/set` Per‑project настройка: перекладывать ли **сетевую** комиссию выплаты на получателя. **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## POST /v1/payout/fee-config/get Тело — пустой объект `{}` без параметров. ```json theme={null} { "state": 0, "result": { "fee_on_recipient": true, "configured": true } } ``` `configured: false` означает, что override не задан и действует дефолт шлюза. *** ## POST /v1/payout/fee-config/set `true` — сетевую комиссию платит получатель (получает меньше); `false` — комиссию несёт мерчант. Ответ: ```json theme={null} { "state": 0, "result": { "fee_on_recipient": true } } ``` *** ## Примеры ```bash cURL theme={null} BODY='{"fee_on_recipient":true}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/fee-config/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/fee-config/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/fee-config/set", {"fee_on_recipient": True}) call("/v1/payout/fee-config/get", {}) ``` ```js Node.js theme={null} await call("/v1/payout/fee-config/set", { fee_on_recipient: true }); await call("/v1/payout/fee-config/get", {}); ``` *** ## Нюансы * Это про **сетевую** комиссию выплаты. За то, кто несёт **нашу** комиссию при возврате, отвечает отдельная настройка — [`refund-fee-config`](/reference/payout-refund-fee-config). * Влияет на распределение сумм в [`POST /v1/payout/calculate`](/reference/payout-calculate) и на фактическую выплату. *** ## Связанные страницы # POST /v1/payout/history Source: https://docs.oblodai.com/reference/payout-history История выплат мерчанта — новые сверху, с limit/offset‑пагинацией. **URL:** `https://api.oblodai.com/v1/payout/history` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Все поля опциональны. Размер страницы. По умолчанию 25, максимум 100. Смещение от начала. Фильтр по **внутреннему** статусу (`pending`, `approved`, `broadcasting`, `sent`, `confirmed`, `failed`, `cancelled`, `awaiting_cosign`) — **не** по укрупнённому `check`/`process`/`paid`. Пусто = все. См. [статусы выплаты](/reference/payout-object#статусы-выплаты). **Фильтр `status` принимает внутренние статусы, а не укрупнённые.** В ответе вы видите укрупнённый статус (например `paid`), но чтобы отобрать такие записи, фильтруйте по внутреннему `confirmed`. Соответствие — в таблице ниже. | Укрупнённый (в ответе) | Внутренний (для фильтра `status`) | | ---------------------------------- | ---------------------------------- | | `check` | `pending` | | `process` | `approved`, `broadcasting`, `sent` | | `paid` | `confirmed` | | `fail` | `failed` | | `cancel` | `cancelled` | | `awaiting_cosign` (без укрупнения) | `awaiting_cosign` | *** ## Пример запроса ```bash cURL theme={null} BODY='{"limit":25,"offset":0}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/history' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/history \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/history", {"limit": 25, "offset": 0}) ``` ```js Node.js theme={null} await call("/v1/payout/history", { limit: 25, offset: 0 }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "items": [ { "uuid": "0c1f...e9", "order_id": "payout-1", "amount": "25", "currency": "USDT", "status": "paid", "is_final": true } ], "paginate": { "count": 42, "per_page": 25, "offset": 0 } } } ``` `paginate.count` — общее число записей по фильтру, не размер страницы. *** ## Связанные страницы # POST /v1/payout/info Source: https://docs.oblodai.com/reference/payout-info Найти выплату по `uuid` (наш) или `order_id` (ваш). **URL:** `https://api.oblodai.com/v1/payout/info` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Нужен хотя бы один идентификатор. Идентификатор выплаты в Oblodai. Ваш номер выплаты. *** ## Пример запроса ```bash cURL theme={null} BODY='{"order_id":"payout-1"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/info' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/info \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/info", {"order_id": "payout-1"}) ``` ```js Node.js theme={null} await call("/v1/payout/info", { order_id: "payout-1" }); ``` *** ## Пример ответа Объект выплаты с актуальным `status` и `txid`. Полное описание — [Объект выплаты](/reference/payout-object). ```json theme={null} { "state": 0, "result": { "uuid": "0c1f...e9", "order_id": "payout-1", "amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "txid": "d0a1...b7", "status": "paid", "is_final": true, "approval_required": false, "source": "api", "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:04:00Z" } } ``` *** ## Коды ошибок | Код | Значение | | ---------------------- | ------------------------------------------- | | `400 payout.bad_uuid` | Некорректный `uuid`. | | `400 payout.no_lookup` | Не передан ни `uuid`, ни `order_id`. | | `404 payout.not_found` | Выплата не найдена (включая чужую выплату). | *** ## Связанные страницы # Выплатные ссылки (крипто-чеки) Source: https://docs.oblodai.com/reference/payout-link Зарезервируйте выплату без адреса получателя — он сам заберёт средства по ссылке. **Методы:** `/v1/payout/link` · `/batch` · `/list` · `/info` · `/cancel` Выплатная ссылка (крипто-чек) — способ отправить средства, **не зная адреса получателя**. Вы резервируете сумму (`available → payout_held` на балансе), получаете одноразовую ссылку `claim_url` и передаёте её получателю (можно письмом — поле `email`). Получатель открывает страницу, вводит свой адрес — и из резерва рождается обычная [выплата](/reference/payout-create). Невостребованная ссылка возвращает резерв при истечении срока или отмене. **URL:** `https://api.oblodai.com/v1/payout/link` · **Аутентификация:** обязательна, **payout-ключ** · **Идемпотентность:** заголовок `Idempotency-Key` здесь **не действует** — дедупликация через поле `reference` (уникально на мерчанта). Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). ## Статусы ссылки | Статус | Значение | | ----------- | ------------------------------------------------------------ | | `funded` | Создана, резерв удержан, ждёт получателя | | `claiming` | Получатель ввёл адрес, выплата порождается (транзиентный) | | `claimed` | Выплата порождена (`payout_id` заполнен) — терминальный | | `expired` | Срок вышел без claim; резерв возвращён — терминальный | | `cancelled` | Отменена мерчантом до claim; резерв возвращён — терминальный | ## POST /v1/payout/link Создаёт одну ссылку. Под row-lock проверяется достаточность **зрелого** баланса (действует [maturity-гейт](/guides/balance-and-funds), как у обычной выплаты). ### Параметры запроса Крипто-актив выплаты (`USDT`, `BTC`, …). Фиат невозможен. Сеть выплаты получателю (`tron`, `bitcoin`, …). Сумма в `currency`, строкой. Больше нуля. Ваш ключ дедупликации, уникальный на мерчанта. Настоятельно рекомендуется: заголовок `Idempotency-Key` на этом эндпоинте не действует. См. предупреждение ниже. Заголовок — виден получателю на странице получения. Сообщение получателю (видно на странице и в письме). Если задан — получателю уходит письмо с кнопкой «Получить средства» (best-effort: ошибка доставки не отменяет создание ссылки). Срок жизни ссылки в часах, клампится в диапазон **1–720** (30 дней). **Задавайте `expires_in_hours` явно.** Если поле не передано (или передан `0`), ссылка живёт всего **1 час** — значение приводится к нижней границе диапазона, а не к максимуму. **Повтор с тем же `reference` сейчас возвращает `500`**, а не повтор ответа: уникальный индекс срабатывает без graceful-реплея. Не ретрайте создание вслепую — сначала проверьте `/v1/payout/link/list`, появилась ли ссылка. В батче дубликат фейлит только свой элемент. ### Пример запроса ```bash cURL wrap theme={null} BODY='{"currency":"USDT","network":"tron","amount":"25","reference":"bonus-42","title":"Бонус","note":"Спасибо за участие","email":"user@example.com","expires_in_hours":168}' TS=$(date +%s) SIGSTR=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/link' "$BODY") SIG=$(printf '%s' "$SIGSTR" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/link \ -X POST \ -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" \ -H "X-Timestamp: $TS" \ -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/link", { "currency": "USDT", "network": "tron", "amount": "25", "reference": "bonus-42", # ваш ключ дедупликации "title": "Бонус", "note": "Спасибо за участие", "email": "user@example.com", # получателю уйдёт письмо со ссылкой "expires_in_hours": 168, # 7 дней; без поля будет ровно 1 час! }) ``` ```js Node.js theme={null} await call("/v1/payout/link", { currency: "USDT", network: "tron", amount: "25", reference: "bonus-42", // ваш ключ дедупликации title: "Бонус", note: "Спасибо за участие", email: "user@example.com", // получателю уйдёт письмо со ссылкой expires_in_hours: 168, // 7 дней; без поля будет ровно 1 час! }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "link_id": "7f0c9c2e-6c2a-4c1e-9f3a-1b2c3d4e5f60", "status": "funded", "amount": "25", "currency": "USDT", "network": "tron", "title": "Бонус", "note": "Спасибо за участие", "reference": "bonus-42", "email": "user@example.com", "expires_at": "2026-07-22T17:00:00Z", "created_at": "2026-07-15T17:00:00Z", "claim_token": "Xk3v…43-символа…", "claim_url": "https://pay.oblodai.com/claim/Xk3v…" } } ``` **`claim_token` и `claim_url` возвращаются только здесь, один раз.** Токен хранится у нас лишь хешем (SHA-256) — восстановить ссылку потом невозможно. Сохраните `claim_url` сразу. ## POST /v1/payout/link/batch До **500** ссылок одним запросом: `{"links": [<элементы как в create>, …]}`. Каждый элемент резервируется в своей транзакции — плохой элемент фейлит только себя. Все ссылки вызова получают общий `batch_id`. ```json Ответ (index-aligned) theme={null} { "state": 0, "result": { "created": 2, "total": 3, "results": [ { "ok": true, "link": { "link_id": "…", "claim_url": "…", "batch_id": "…" } }, { "ok": false, "error": "payoutlink.insufficient_funds", "message": "available balance is less than the link amount" }, { "ok": true, "link": { "…": "…" } } ] } } ``` Ошибки уровня запроса: `payoutlink.empty_batch` (400), `payoutlink.batch_too_large` (400, больше 500). ## POST /v1/payout/link/list Запрос: `{"limit": 50, "offset": 0}` (limit вне 1–200 → 50). Сортировка — новые первыми. Ответ: `{"links": [ … ]}` — **без** `claim_token`/`claim_url`. ## POST /v1/payout/link/info Запрос: `{"link_id": ""}`. После claim в ответе появляются `payout_id` и `claim_address`. ## POST /v1/payout/link/cancel Запрос: `{"link_id": ""}`. Отменяется только `funded`-ссылка: резерв возвращается (`payout_held → available`), статус — `cancelled`. Если получатель успел завершить claim конкурентно, вернётся ссылка со статусом `claimed` и `payout_id` — деньги уже ушли, двойного возврата не бывает. ## Коды ошибок Неизвестный крипто-актив. **Повтор:** Нет — проверьте пару валюта/сеть. Некорректная сумма. **Повтор:** Нет. Недостаточно доступного баланса. **Повтор:** Да, после пополнения. Средства ещё дозревают. **Повтор:** Да, позже с backoff — как у выплат. Пустой массив `links`. **Повтор:** Нет. Больше 500 элементов — разбейте на страницы. **Повтор:** Нет. Некорректный `link_id`. **Повтор:** Нет. Ссылка не найдена (или чужая). **Повтор:** Нет — сверьте `link_id`. Отменить можно только невостребованную (`funded`) ссылку. **Повтор:** Нет — проверьте статус через `/info`. Функция выключена на шлюзе. **Повтор:** Нет — обратитесь в поддержку. ## Нюансы * **Письмо получателю** — брендовый шаблон с кнопкой «Получить средства», текст `note` включается в письмо. Отправка best-effort: сбой почты не отменяет резерв. * **Поллеры**: истечение проверяется раз в 60 секунд; «зависший» claim (статус `claiming` дольше 15 минут) добирается рипером — либо финализируется в `claimed`, либо откатывается в `funded`. * **Вебхуки**: отдельных событий у ссылок нет. Когда получатель забирает средства, порождённая выплата шлёт обычные [`payout.*`](/reference/webhook-object) события; её `order_id` = `payoutlink:` — так вы сопоставите событие со ссылкой. * **Баланс**: резерв виден как удержание `payout_held`; `available` уменьшается в момент создания ссылки. См. [Модель баланса](/guides/balance-and-funds). ## Связанные страницы # POST /v1/payout/mass Source: https://docs.oblodai.com/reference/payout-mass Массовая выплата — до **100** выплат за один запрос. Каждый элемент независим: ошибка по одному не останавливает остальные. Результаты приходят **синхронно**, прямо в ответе. **Больше 100 выплат — используйте [`POST /v1/payout/batch`](/reference/payout-batch).** Батч принимает до **5000** элементов, обрабатывается асинхронно (`batch_id` + поллинг [`/v1/batch/info`](/reference/batch-info)) и остаётся **одним** запросом в бюджете [лимита частоты](/reference/basics-ratelimit). `/v1/payout/mass` — легаси‑путь для маленьких пачек, когда результат нужен немедленно. **URL:** `https://api.oblodai.com/v1/payout/mass` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Массив ≤100 элементов; поля каждого — как в [`POST /v1/payout`](/reference/payout-create). Метка происхождения, применяется ко всем элементам без своего `source`. *** ## Пример запроса ```bash cURL theme={null} BODY='{"payouts":[ {"amount":"25","currency":"USDT","network":"tron","address":"TXY...","order_id":"p-1"}, {"amount":"10","currency":"USDT","network":"tron","address":"TZZ...","order_id":"p-2"} ]}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/mass' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/mass \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/mass", {"payouts": [ {"amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "order_id": "p-1"}, {"amount": "10", "currency": "USDT", "network": "tron", "address": "TZZ...", "order_id": "p-2"}, ]}) ``` ```js Node.js theme={null} await call("/v1/payout/mass", { payouts: [ { amount: "25", currency: "USDT", network: "tron", address: "TXY...", order_id: "p-1" }, { amount: "10", currency: "USDT", network: "tron", address: "TZZ...", order_id: "p-2" }, ] }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "items": [ { "uuid": "…", "order_id": "p-1", "status": "process", "is_final": false, "approval_required": false, "success": true }, { "order_id": "p-2", "success": false, "message": "available balance is less than the requested amount" } ] } } ``` * Успешный элемент содержит `uuid`, `status`, `is_final`, `approval_required`, `success: true`. * Неуспешный — `success: false` и текст в `message`. У элементов `items[]` есть только `success` и текст `message` — машиночитаемого `code` у отдельного элемента **НЕТ** (в отличие от общего конверта ошибки). Для программной логики ориентируйтесь на `success`, а `message` используйте для логов/диагностики. *** ## Коды ошибок Ошибки **уровня запроса** (весь батч отклонён): | Код | Значение | | ---------------------------- | ------------------------ | | `400 request.bad_json` | Тело не парсится. | | `400 payout.empty_batch` | Пустой массив `payouts`. | | `400 payout.batch_too_large` | Больше 100 элементов. | Ошибки **отдельных выплат** приходят в `items[].message` при `success: false`. *** ## Нюансы * Каждый элемент идемпотентен по своему `order_id`. * Частичный успех — норма: обрабатывайте `items` поэлементно, а не «весь батч прошёл / не прошёл». *** ## Связанные страницы до 5000 выплат, основной путь для больших пачек. # Объект выплаты (Payout) Source: https://docs.oblodai.com/reference/payout-object Объект выплаты описывает вывод средств на внешний адрес. Формируется при создании выплаты ([`POST /v1/payout`](/reference/payout-create)) и возвращается методами [`/info`](/reference/payout-info) и [`/history`](/reference/payout-history). *** ## Пример объекта ```json theme={null} { "uuid": "0c1f...e9", "order_id": "payout-1", "amount": "25", "currency": "USDT", "network": "tron", "address": "TXY...", "txid": "", "status": "process", "is_final": false, "approval_required": false, "source": "api", "created_at": "2026-07-10T12:00:00Z", "updated_at": "2026-07-10T12:00:00Z" } ``` Идентификатор выплаты в Oblodai. Ваш номер выплаты; ключ идемпотентности. Сумма выплаты в `currency`. Код валюты. Сеть. Адрес получателя. Хеш транзакции; пуст до отправки в сеть. Укрупнённый статус. См. [таблицу](#статусы-выплаты). Терминальный статус (`paid`/`fail`/`cancel`). Нужно ли ручное подтверждение. Для выплат по API‑ключу — `false`. Метка происхождения: `api` или `manual`. Время создания (ISO 8601). Время последнего изменения (ISO 8601). Только в ответе создания выплаты с `from_currency` — детали конвертации баланса: ```json theme={null} { "from_currency": "USDT", "to_currency": "TRX", "from_amount": "40.12", "rate": "0.062" } ``` Валюта, с которой конвертировали баланс. Валюта выплаты (равна `currency`). Списанная сумма в `from_currency`. Курс конвертации. *** ## Статусы выплаты Внутренний жизненный цикл — `pending → approved → (awaiting_cosign) → (broadcasting) → sent → confirmed` (либо `failed` / `cancelled`). Но **в поле `status` ответов приходит укрупнённый (Heleket‑совместимый) статус**: | Внутренний статус | `status` в ответе | Значение | | ---------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `pending` | `check` | Создана, средства зарезервированы, ждёт одобрения. | | `approved`, `broadcasting`, `sent` | `process` | Одобрена / отправляется / отправлена, ждёт подтверждений. | | `confirmed` | `paid` | Подтверждена в блокчейне — готово. | | `awaiting_cosign` | `awaiting_cosign` | Ждёт второй подписи (2‑of‑2). Приходит **как есть** — своего укрупнённого статуса нет. Только для мерчантов с обязательным co‑sign. | | `failed` | `fail` | Отклонена / отправка не удалась, резерв освобождён. | | `cancelled` | `cancel` | Отменена до отправки, резерв освобождён. | Признак финальности — `is_final: true` (статусы `paid` / `fail` / `cancel`). *** ## Важное про одобрение * **По API‑ключу выплата авто‑одобряется** (`approval_required: false`) и сразу уходит в отправку — без белых списков и периодов выдержки. Ответственность за адрес назначения на вашей стороне. * Статус `pending` + `approval_required: true` возникает только у внутренних/кабинетных сценариев (например, возврат на адрес, отличный от адреса плательщика) и подтверждается через [`POST /v1/payout/approve`](/reference/payout-approve). Обычной интеграции по API‑ключу этот шаг не нужен. *** ## Связанные страницы # Комиссия возврата Source: https://docs.oblodai.com/reference/payout-refund-fee-config **Методы:** `/v1/payout/refund-fee-config/get` · `/v1/payout/refund-fee-config/set` Per‑project настройка: кто несёт **нашу** комиссию при возврате. Шлюз её никогда не покрывает — её платит либо клиент (получает net), либо мерчант (клиент получает gross). **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## POST /v1/payout/refund-fee-config/get Тело — `{}`. ```json theme={null} { "state": 0, "result": { "fee_on_customer": false, "configured": false } } ``` `configured: false` — override не задан, действует дефолт шлюза. *** ## POST /v1/payout/refund-fee-config/set `true` — клиент получает **net** (комиссию платит клиент); `false` — мерчант платит комиссию, клиент получает **gross**. Ответ: ```json theme={null} { "state": 0, "result": { "fee_on_customer": true } } ``` *** ## Примеры ```bash cURL theme={null} BODY='{"fee_on_customer":true}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/refund-fee-config/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/refund-fee-config/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/payout/refund-fee-config/set", {"fee_on_customer": True}) call("/v1/payout/refund-fee-config/get", {}) ``` ```js Node.js theme={null} await call("/v1/payout/refund-fee-config/set", { fee_on_customer: true }); await call("/v1/payout/refund-fee-config/get", {}); ``` *** ## Нюансы * Это про **нашу** комиссию при **возврате** ([`/v1/payment/refund`](/reference/payment-refund)). Не путайте с [`fee-config`](/reference/payout-fee-config), которая про **сетевую** комиссию **выплаты**. * Шлюз свою комиссию при возврате не покрывает — выбор только между клиентом и мерчантом. *** ## Связанные страницы # POST /v1/payout/services Source: https://docs.oblodai.com/reference/payout-services Список доступных методов выплат (пар валюта + сеть) с флагом доступности, лимитами и комиссиями. **URL:** `https://api.oblodai.com/v1/payout/services` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Тело:** пустой объект `{}` — параметров нет. Метод доступен, если под пару `(сеть, валюта)` подключён реальный он‑чейн‑отправитель. *** ## Пример запроса ```bash cURL theme={null} TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/payout/services' '{}' \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/payout/services \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d '{}' ``` ```python Python theme={null} call("/v1/payout/services", {}) ``` ```js Node.js theme={null} await call("/v1/payout/services", {}); ``` *** ## Пример ответа Форма — как у [`POST /v1/payment/services`](/reference/payment-services): массив объектов с `network`, `currency`, `is_available`, `limit`, `commission`. ```json theme={null} { "state": 0, "result": [ { "network": "tron", "currency": "USDT", "is_available": true, "limit": { "min_amount": "0", "max_amount": "" }, "commission": { "fee_amount": "0", "percent": "0" } }, { "network": "ethereum", "currency": "ETH", "is_available": true, "limit": { "min_amount": "0", "max_amount": "" }, "commission": { "fee_amount": "0", "percent": "0" } } ] } ``` **`commission`** здесь — **сетевая** комиссия (газ сети), а **НЕ** комиссия платформы Oblodai. Платформенная комиссия (по умолчанию 1.5 % + \$0.30/платёж) удерживается при приёме платежа, а не при выплате. *** ## Нюансы * Проверяйте `is_available` перед созданием выплаты — метод доступен только при подключённом он‑чейн‑отправителе для пары `(сеть, валюта)`. *** ## Связанные страницы аналог для приёма. # POST /v1/referral/info Source: https://docs.oblodai.com/reference/referral-info Реферальная панель: код, ссылка, число приведённых мерчантов и суммарные начисления. **URL:** `https://api.oblodai.com/v1/referral/info` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Тело:** пустой объект `{}` — параметров нет. *** ## Пример запроса ```bash cURL theme={null} TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/referral/info' '{}' \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/referral/info \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d '{}' ``` ```python Python theme={null} call("/v1/referral/info", {}) ``` ```js Node.js theme={null} await call("/v1/referral/info", {}); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "code": "K7QF2M9A", "link": "https://oblodai.com/?ref=K7QF2M9A", "tier_bps": [3000, 2000, 1000], "referred_count": 4, "earnings_by_asset": { "USDT": "18450000", "ETH": "3200000000000000" }, "week": { "referred_count": 1, "earnings_by_asset": { "USDT": "250000" } } } } ``` Персональный реферальный код. Готовая пригласительная ссылка `…/?ref=`. Доля **нашей** комиссии рефереру по месяцам, в bps. По умолчанию `[3000, 2000, 1000]` = 30 % / 20 % / 10 % за 1‑й, 2‑й, 3‑й месяц, дальше 0. Сколько мерчантов приведено. Валюта → сумма в **минимальных единицах** (minor units), строкой. То же за скользящие **7 дней**: `referred_count` и `earnings_by_asset` (minor units). Реорг, отозвавший начисление, автоматически уменьшает и недельную сумму. *** ## Нюансы * Приведённый мерчант получает **сниженную** комиссию (по умолчанию 1.4 %); реферер зарабатывает долю комиссии платформы 3 месяца. * `earnings_by_asset` — в **minor‑единицах**. Для USDT (6 знаков) `"18450000"` = 18.45 USDT. См. [Форматы сумм и денег](/reference/basics-money). *** ## Связанные страницы # POST /v1/refund/batch Source: https://docs.oblodai.com/reference/refund-batch Массовый возврат — до **5000** возвратов одним запросом. Обрабатывается асинхронно: в ответ сразу приходит `batch_id`, результат по каждому элементу забирается через [`POST /v1/batch/info`](/reference/batch-info). **Это штатный способ не упираться в rate limit.** Один подписанный запрос вместо 5000 — [лимит частоты](/reference/basics-ratelimit) считается по запросам, а не по элементам внутри них. **URL:** `https://api.oblodai.com/v1/refund/batch` · **Аутентификация:** обязательна · **Идемпотентность:** заголовок `Idempotency-Key` (на весь батч) + тройка `(платёж, адрес, сумма)` каждого элемента. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Массив от 1 до **5000** элементов. Поля каждого элемента — **ровно те же**, что у [`POST /v1/payment/refund`](/reference/payment-refund). Что делать при ошибке элемента: `continue` (по умолчанию) — обрабатывать остальные; `stop` — прекратить обработку после первой ошибки. Каждый элемент проходит **тот же** путь, что и одиночный возврат: `address` необязателен (по умолчанию — [`payer_address`](/reference/payment-object) платежа), возврат на адрес плательщика авто‑подтверждается, на любой другой — ждёт [подтверждения](/reference/payout-approve). *** ## Пример запроса ```bash cURL theme={null} BODY='{"refunds":[ {"order_id":"order-1"}, {"order_id":"order-2","amount":"5"}, {"uuid":"a1b2...c3","address":"TZZ...","network":"tron"} ],"on_error":"continue"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/refund/batch' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/refund/batch \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -H "Idempotency-Key: refunds-2026-07-13" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/refund/batch", { "refunds": [ {"order_id": "order-1"}, # весь платёж, на адрес плательщика {"order_id": "order-2", "amount": "5"}, # частичный возврат {"uuid": "a1b2...c3", "address": "TZZ...", "network": "tron"}, # на явный адрес ], "on_error": "continue", }) ``` ```js Node.js theme={null} await call("/v1/refund/batch", { refunds: [ { order_id: "order-1" }, { order_id: "order-2", amount: "5" }, { uuid: "a1b2...c3", address: "TZZ...", network: "tron" }, ], on_error: "continue", }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "batch_id": "3c7e9a10-5b21-4f8d-9a44-6d2e1f0b7c39", "kind": "refund", "count": 3, "status": "pending" } } ``` Идентификатор батча — с ним в [`POST /v1/batch/info`](/reference/batch-info). Вид батча — здесь всегда `refund`. Сколько элементов принято в обработку. Стартовый статус — всегда `pending`. *** ## Статусы батча `pending` → `processing` → `completed`. Подробнее — [`/v1/batch/info`](/reference/batch-info). *** ## Коды ошибок | Код | Значение | | ---------------------- | -------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 batch.empty` | Массив `refunds` пуст. | | `400 batch.too_large` | Больше 5000 элементов. | | `503 batch.disabled` | Массовая обработка недоступна на этом шлюзе. | | `401 auth.*` | Ошибки аутентификации. | Ошибки отдельных возвратов (`refund.nothing_to_refund`, `refund.no_address`, `refund.exceeds_refundable` и т. п.) приходят в `items[].error` ответа [`/v1/batch/info`](/reference/batch-info). *** ## Нюансы * **Баланс проверяется поэлементно.** Возврат — это списание с баланса. Если средств не хватит, часть элементов упадёт с ошибкой, а остальные пройдут (при `on_error: "continue"`). * **Возврат на чужой адрес** останется ждать подтверждения ([maker‑checker](/reference/payout-approve)) — батч этого не отменяет. * **Bitcoin/UTXO.** У таких платежей нет единого отправителя, `payer_address` пуст — в элементе обязательно указывайте `address`, иначе получите `refund.no_address`. *** ## Связанные страницы поллинг статуса и результатов. поля элемента. # POST /v1/split/config/get · /set Source: https://docs.oblodai.com/reference/split-config Настройка **окна удержания** (`refund_hold_hours`) — на сколько часов откладывается расчёт по [сплит‑правилам](/reference/split-rule). **Базовый URL:** `https://api.oblodai.com` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Зачем откладывать расчёт Это главное, что нужно понять про сплиты. **Возврат списывает с вас ВСЮ сумму, которую заплатил покупатель.** Не «вашу долю» — всю. Покупатель заплатил \$100, вы возвращаете \$100. Теперь представьте, что доли партнёрам разошлись сразу: 1. Покупатель платит **\$100**. 2. Сплит‑правила мгновенно отправляют **\$30** партнёрам на их адреса в блокчейне. 3. У вас на балансе остаётся **\$70**. 4. Покупатель просит возврат. Вернуть надо **\$100**, а есть **\$70**. **\$30 уже в блокчейне, и достать их оттуда невозможно.** Чтобы такого не было, доли партнёрам уходят **не сразу**, а спустя `refund_hold_hours` после того, как платёж зачтён. Пока окно не закрылось, деньги лежат у вас, и любой возврат берётся из них. База сплита пересчитывается на момент фактической отправки как **«пришло минус возвращено»**: возврат внутри окна уменьшает долю партнёра или обнуляет её совсем. Ставьте окно не меньше, чем реальный срок, за который у вас случаются возвраты (например 48–72 часа). **`refund_hold_hours` — это не бюрократическая задержка, а окно, в течение которого деньги ещё можно вернуть покупателю.** *** ## Что происходит при возврате | Когда пришёл возврат | Что с долей партнёра | | -------------------------------------------------- | ------------------------------------------------------------------------------------------ | | **Внутри окна, возврат полный** | Доля **отменяется целиком** — партнёру не уходит ничего. | | **Внутри окна, возврат частичный** | Доля **пересчитывается вниз**: база сплита = «пришло минус возвращено». | | **После окна, партнёр внутренний** (`merchant_id`) | Доля **отзывается обратно** (claw‑back), пропорционально сумме возврата. | | **После окна, партнёр внешний** (`address`) | **Вернуть нельзя.** Деньги в блокчейне. Возврат покупателю вы покрываете из своих средств. | Отсюда два вывода: * **Внутренние сплиты безопасны всегда** — платформа сама заберёт долю у партнёра, даже если окно уже прошло. * **Внешние сплиты защищены только окном.** Как только оно истекло и транзакция ушла в сеть, откатить её невозможно — ни вам, ни нам, ни партнёру. **`refund_hold_hours: 0` + внешний сплит = возврат за ваш счёт.** Доли уйдут почти сразу, и если покупатель попросит возврат, вернуть его долю будет **физически невозможно** — разницу придётся покрывать из собственных средств. Ставьте `0`, только если возвратов у вас не бывает в принципе или все ваши сплиты внутренние. *** ## POST /v1/split/config/get Параметров нет — пошлите `{}`. ### Пример запроса ```bash cURL theme={null} BODY='{}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/split/config/get' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/split/config/get \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/split/config/get", {}) ``` ```js Node.js theme={null} await call("/v1/split/config/get", {}); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "refund_hold_hours": 48 } } ``` *** ## POST /v1/split/config/set ### Параметры запроса На сколько часов откладывать расчёт по сплитам. Диапазон **0–2160** (до 90 суток). `0` — отправлять доли сразу, **риск невозможности возврата берёте на себя**. ### Пример запроса ```bash cURL theme={null} BODY='{"refund_hold_hours":48}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/split/config/set' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/split/config/set \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/split/config/set", {"refund_hold_hours": 48}) ``` ```js Node.js theme={null} await call("/v1/split/config/set", { refund_hold_hours: 48 }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "refund_hold_hours": 48 } } ``` *** ## Коды ошибок | Код | Значение | | ---------------------- | ----------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 split.bad_hold` | `refund_hold_hours` вне диапазона 0–2160. | | `503 split.disabled` | Сплит‑платежи недоступны на этом шлюзе. | | `401 auth.*` | Ошибки аутентификации. | *** ## Нюансы * **Окно влияет и на другие исходящие маршруты** проекта — [автовывод](/reference/auto-withdraw) и авто‑конвертацию [VRCS](/reference/vrcs): они тоже откладываются, и по той же причине. * **Изменение окна действует на будущие платежи.** Доли, уже поставленные в очередь на отправку, свой срок отсчитывают по старому значению. * **Настройка общая на проект**, а не на правило: единое окно для всех сплитов. *** ## Связанные страницы сами правила распределения. возврат и его влияние на доли. # Правила сплита Source: https://docs.oblodai.com/reference/split-rule **Методы:** `/v1/split/rule` · `/rule/list` · `/rule/delete` **Сплит‑платёж** — правило, по которому доля каждого входящего платежа автоматически уходит партнёру. Пример: из каждых \$100 — 70 % остаётся вам, 20 % уходит на адрес A, 10 % на адрес B. Типовые сценарии: партнёрские программы, реселлеры, доля площадки в маркетплейсе, разделение выручки между соучредителями. Правила действуют на **все** входящие платежи мерчанта — задавать их у каждого счёта не нужно. **Базовый URL:** `https://api.oblodai.com` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Расчёт по сплитам не мгновенный.** Он откладывается на окно удержания `refund_hold_hours` — см. [`POST /v1/split/config/get · /set`](/reference/split-config). Прочитайте, **почему**: это не задержка ради задержки, а защита от ситуации «деньги ушли партнёру, а покупатель попросил возврат». *** ## Два вида получателя Это **главное** решение при создании правила: от него зависит, сможете ли вы вернуть деньги покупателю. | Получатель | Как задаётся | Как уходит доля | Что при возврате | | -------------------------- | --------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **Мерчант внутри Oblodai** | `merchant_id` | Движение по внутреннему учёту (ledger). | **Обратимо:** доля **отзывается обратно** (claw‑back), пропорционально сумме возврата — даже если окно удержания уже прошло. | | **Внешний адрес** | `address` + `network` | Реальная транзакция в блокчейне. | **НЕОБРАТИМО:** транзакция ушла в сеть, вернуть её нельзя. Возврат покупателю вы покрываете из своих средств. | Задавайте **либо** `address` + `network`, **либо** `merchant_id` — не оба сразу. **Внутренний сплит безопаснее.** Если покупатель попросит возврат, платформа сама заберёт долю у партнёра. Внешний сплит уходит в блокчейн навсегда — от него вас защищает **только** окно удержания [`refund_hold_hours`](/reference/split-config): пока оно не истекло, доля партнёру ещё не отправлена, и возврат покупателю возможен целиком. Полный возврат внутри окна **отменяет** сплит, частичный — **уменьшает** долю партнёра. **Не ставьте `refund_hold_hours: 0` при внешних сплитах.** Доли уйдут почти сразу, и вернуть их станет физически невозможно. → [`POST /v1/split/config/set`](/reference/split-config) *** ## POST /v1/split/rule — создать правило ### Параметры запроса Доля от каждого платежа: `10` = 10 %, `2.5` = 2.5 %. Больше 0 и не больше 100. Точность — до 0.01 %. Внешний криптоадрес партнёра. Сеть адреса. **Обязательна вместе с `address`.** Идентификатор мерчанта‑партнёра внутри Oblodai. Комментарий для себя (в списке правил). Ровно один вариант получателя: либо `address` + `network`, либо `merchant_id`. **Сумма всех правил не может превышать 100 %** — иначе `split.exceeds_100`. Себе сплит назначить нельзя (`split.self_destination`). ### Пример запроса ```bash cURL theme={null} BODY='{"address":"TXY...","network":"tron","percent":10,"note":"партнёр Алиса"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/split/rule' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/split/rule \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} # 10 % на внешний адрес — необратимо. call("/v1/split/rule", { "address": "TXY...", "network": "tron", "percent": 10, "note": "партнёр Алиса", }) # 20 % мерчанту внутри платформы — при возврате доля отзовётся обратно. call("/v1/split/rule", { "merchant_id": "b4c1f0e2-5a77-4d31-9f08-2c6e7a1b3d94", "percent": 20, "note": "площадка", }) ``` ```js Node.js theme={null} // 10 % на внешний адрес — необратимо. await call("/v1/split/rule", { address: "TXY...", network: "tron", percent: 10, note: "партнёр Алиса", }); // 20 % мерчанту внутри платформы — при возврате доля отзовётся обратно. await call("/v1/split/rule", { merchant_id: "b4c1f0e2-5a77-4d31-9f08-2c6e7a1b3d94", percent: 20, note: "площадка", }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "rule_id": "e2a91c47-3f80-4b6d-a512-7c0d9e8f1a23", "percent": 10 } } ``` *** ## POST /v1/split/rule/list — список правил Параметров нет — пошлите `{}`. ```python Python theme={null} call("/v1/split/rule/list", {}) ``` ```js Node.js theme={null} await call("/v1/split/rule/list", {}); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "items": [ { "rule_id": "e2a91c47-3f80-4b6d-a512-7c0d9e8f1a23", "percent": 10, "active": true, "note": "партнёр Алиса", "address": "TXY...", "network": "tron", "reversible": false }, { "rule_id": "7b3d0f19-6c25-4e88-b0a4-1f9e2d5c8a07", "percent": 20, "active": true, "note": "площадка", "merchant_id": "b4c1f0e2-5a77-4d31-9f08-2c6e7a1b3d94", "reversible": true } ] } } ``` Идентификатор правила. Доля от каждого платежа, %. Действует ли правило. Ваш комментарий. Получатель — внешний адрес (только у внешних правил). Сеть внешнего адреса (только у внешних правил). Получатель — мерчант платформы (только у внутренних правил). `true` — доля отзывается при возврате платежа (внутренний сплит); `false` — уходит в блокчейн навсегда (внешний). *** ## POST /v1/split/rule/delete — удалить правило ### Параметры запроса Идентификатор правила. ```python Python theme={null} call("/v1/split/rule/delete", {"rule_id": "e2a91c47-3f80-4b6d-a512-7c0d9e8f1a23"}) ``` ```js Node.js theme={null} await call("/v1/split/rule/delete", { rule_id: "e2a91c47-3f80-4b6d-a512-7c0d9e8f1a23" }); ``` ### Пример ответа ```json theme={null} { "state": 0, "result": { "deleted": true } } ``` Удаление действует на **будущие** платежи. Доли, уже начисленные по прошлым платежам и ждущие окончания окна удержания, всё равно уйдут партнёру. *** ## Коды ошибок | Код | Значение | | ---------------------------- | --------------------------------------------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 split.bad_destination` | Не задан получатель, либо заданы **оба** (`address` и `merchant_id`). | | `400 split.network_required` | Задан `address`, но не задана `network`. | | `400 split.bad_percent` | `percent` вне диапазона 0–100. | | `400 split.exceeds_100` | Сумма долей всех правил превысила 100 %. | | `400 split.bad_merchant` | `merchant_id` не является корректным UUID. | | `400 split.self_destination` | Получатель — вы сами. | | `400 split.bad_id` | `rule_id` не является корректным UUID. | | `404 split.not_found` | Правило не найдено. | | `503 split.disabled` | Сплит‑платежи недоступны на этом шлюзе. | | `401 auth.*` | Ошибки аутентификации. | *** ## Нюансы * **Сплит — пятый путь ухода средств** с вашего баланса (наряду с [выплатой](/reference/payout-create), [возвратом](/reference/payment-refund), [автовыводом](/reference/auto-withdraw) и [переводом на личный кошелёк](/reference/transfer-to-personal)). Учитывайте его, когда сводите баланс. * **База расчёта — то, что реально пришло, за вычетом возвратов.** Доля считается от суммы платежа на момент исполнения, а не от суммы счёта: возврат внутри окна удержания уменьшает или обнуляет долю. * **Доля уходит в монете платежа.** Отдельной конвертации сплит не делает. * **Внешние сплиты необратимы.** Если вам нужна возможность отката, используйте `merchant_id` (внутренний сплит) — либо держите достаточное окно [`refund_hold_hours`](/reference/split-config). *** ## Связанные страницы окно удержания `refund_hold_hours` и **почему** оно нужно. как возврат взаимодействует со сплитами. # POST /v1/transfer/to-personal Source: https://docs.oblodai.com/reference/transfer-to-personal Перевести средства с бизнес‑кошелька мерчанта на **личный** кошелёк владельца аккаунта. **Зачем это.** Метод забирает прибыль владельца из общего котла магазинов и переводит её на его личный кошелёк. Сам вывод с личного кошелька наружу — отдельная операция, доступная только в кабинете под 2FA (на API‑ключе её нет). **URL:** `https://api.oblodai.com/v1/transfer/to-personal` · **Аутентификация:** обязательна · **Идемпотентность:** заголовок `Idempotency-Key` и/или `order_id`. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). ### Заголовки Любое уникальное значение (≤255 символов), одинаковое во всех повторах одного перевода. Повтор вернёт тот же ответ и заголовок `Idempotent-Replayed: true`. → [Идемпотентность](/reference/basics-idempotency) *** ## Параметры запроса Сумма перевода в `currency`. Код валюты. Ключ идемпотентности. Повтор — no‑op. **Настоятельно передавайте всегда** (см. ниже). *** ## Пример запроса ```bash cURL theme={null} BODY='{"amount":"50","currency":"USDT","order_id":"transfer-1"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/transfer/to-personal' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/transfer/to-personal \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/transfer/to-personal", {"amount": "50", "currency": "USDT", "order_id": "transfer-1"}) ``` ```js Node.js theme={null} await call("/v1/transfer/to-personal", { amount: "50", currency: "USDT", order_id: "transfer-1" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "currency": "USDT", "amount": "50", "direction": "to_personal", "personal_balance": "150" } } ``` *** ## Коды ошибок | Код | Значение | | --------------------------------- | ---------------------------------------------------- | | `400 transfer.no_personal_wallet` | У мерчанта нет привязанного владельца. | | `400 transfer.unknown_currency` | Неизвестная валюта. | | `400 transfer.bad_amount` | Некорректная сумма. | | `400 personal.amount_invalid` | Сумма перевода некорректна (≤0 или неверный формат). | | `400 personal.insufficient` | Недостаточно средств на балансе для перевода. | | `400 request.bad_json` | Тело не парсится. | *** ## Нюансы * **Всегда передавайте `order_id`.** Формально поле необязательное, но без него повтор запроса при сетевом таймауте создаст **второй перевод**. С `order_id` повтор гарантированно no‑op. * Перевод **вводит средства В личный кошелёк**. Обратная операция и **вывод из личного кошелька доступны только в кабинете под 2FA** — на API‑ключе их намеренно нет. * Личный кошелёк **общий** для всех магазинов владельца. *** ## Связанные страницы # POST /v1/vrcs Source: https://docs.oblodai.com/reference/vrcs Включает или выключает **VRCS** (Volatility Risk Control System) — авто‑конвертацию волатильных депозитов в USDT. Без поля `enabled` работает как getter. **URL:** `https://api.oblodai.com/v1/vrcs` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). VRCS защищает от колебаний курса: как только на баланс приходит волатильный актив, он автоматически конвертируется в USDT. *** ## Параметры запроса Включить/выключить VRCS. **Опустите поле для чтения** текущего состояния. *** ## Пример запроса ```bash cURL theme={null} # Включить BODY='{"enabled":true}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/vrcs' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/vrcs \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/vrcs", {"enabled": True}) # запись call("/v1/vrcs", {}) # чтение ``` ```js Node.js theme={null} await call("/v1/vrcs", { enabled: true }); // запись await call("/v1/vrcs", {}); // чтение ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "enabled": true } } ``` *** ## Коды ошибок | Код | Значение | | --------------- | ------------------------ | | `400 vrcs.read` | Ошибка чтения состояния. | *** ## Нюансы * Настройка привязана к **мерчанту**, а не к проекту. * Без поля `enabled` вызов не меняет состояние, а только возвращает текущее. *** ## Связанные страницы # POST /v1/wallet/block Source: https://docs.oblodai.com/reference/wallet-block Заблокировать или разблокировать статический кошелёк по адресу. Заблокированный кошелёк перестаёт зачислять новые пополнения. **URL:** `https://api.oblodai.com/v1/wallet/block` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Адрес статического кошелька. `true` — заблокировать (значение по умолчанию, если поле опущено); `false` — снять блокировку. *** ## Пример запроса ```bash cURL theme={null} # Заблокировать BODY='{"address":"TXk9...c3Fd"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/wallet/block' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/wallet/block \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/wallet/block", {"address": "TXk9...c3Fd"}) # заблокировать call("/v1/wallet/block", {"address": "TXk9...c3Fd", "is_force_block": False}) # разблокировать ``` ```js Node.js theme={null} await call("/v1/wallet/block", { address: "TXk9...c3Fd" }); // заблокировать await call("/v1/wallet/block", { address: "TXk9...c3Fd", is_force_block: false }); // разблокировать ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "uuid": "0e5b6b9a-…", "address": "TXk9...c3Fd", "blocked": true } } ``` *** ## Коды ошибок | Код | Значение | | ----------------------------- | --------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 wallet.no_address` | Не передан `address`. | | `404 wallet.static_not_found` | Кошелёк не найден. | *** ## Нюансы * `is_force_block` по умолчанию `true`. **Чтобы снять блокировку, явно передайте `false`.** * Операция скоуплена мерчантом — заблокировать можно только свой кошелёк. * Средства, уже полученные на заблокированный кошелёк, можно вернуть через [`POST /v1/wallet/blocked-address-refund`](/reference/wallet-blocked-refund). *** ## Связанные страницы # POST /v1/wallet/blocked-address-refund Source: https://docs.oblodai.com/reference/wallet-blocked-refund Вернуть средства, полученные на (обычно заблокированном) статическом кошельке, на один указанный адрес. Возвращается **чистая** сумма — та, что фактически была зачислена на баланс мерчанта: уже за вычетом комиссии платформы, удержанной при зачислении депозитов (ставка для статических кошельков, по умолчанию \~1.5% — см. [Статические кошельки](/guides/static-wallets)), и за вычетом отменённых [reorg](/reference/glossary)‑депозитов — депозитов, откатившихся при переписывании недавних блоков сети. По сути это списание с баланса — выплата; однократно и идемпотентно на уровне кошелька. **URL:** `https://api.oblodai.com/v1/wallet/blocked-address-refund` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Идентификатор статического кошелька (из ответа [`/v1/wallet`](/reference/wallet-create)). Адрес назначения возврата. *** ## Пример запроса ```bash cURL theme={null} BODY='{"uuid":"0e5b6b9a-…","address":"TYr2...9kQp"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/wallet/blocked-address-refund' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/wallet/blocked-address-refund \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/wallet/blocked-address-refund", {"uuid": "0e5b6b9a-…", "address": "TYr2...9kQp"}) ``` ```js Node.js theme={null} await call("/v1/wallet/blocked-address-refund", { uuid: "0e5b6b9a-…", address: "TYr2...9kQp" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "uuid": "b71c2d3e-…", "wallet_uuid": "0e5b6b9a-…", "amount": "149.50", "commission": "0.50", "currency": "USDT", "address": "TYr2...9kQp", "status": "check", "is_final": false } } ``` Идентификатор операции возврата. Кошелёк, с которого возвращаются средства. Возвращаемая чистая сумма. Удержанный сетевой газ. Валюта возврата. Адрес назначения. Статус операции (как у выплаты). Достигнут ли [терминальный статус](/reference/glossary) (конечное состояние, объект больше не изменится). *** ## Коды ошибок | Код | Значение | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `400 request.bad_json` | Тело не парсится. | | `400 refund.no_address` | Не передан адрес назначения. | | `400 wallet.bad_uuid` | Некорректный `uuid`. | | `400 refund.nothing_to_refund` | На кошельке нет средств к возврату. | | `400 refund.destination_internal` | Адрес назначения принадлежит шлюзу ([self‑dealing](/reference/glossary) — выплата на собственный адрес шлюза запрещена). | | `400 refund.dust` | Сумма слишком мала ([dust](/reference/glossary) — не покрывает даже сетевую комиссию). | | `404 wallet.static_not_found` | Кошелёк не найден. | Адрес назначения дополнительно проходит проверку формата под сеть и комплаенс‑скрининг. *** ## Нюансы * Возвращается только **чистая** сумма — reorg‑отменённые депозиты исключаются. * При самом возврате удерживается только сетевой газ (`commission`), который вычитается из `amount` (получатель получает `amount − commission`); комиссия платформы уже была удержана при зачислении и в базу возврата не входит. * Идемпотентно по кошельку (ключ `refund-wallet:`). *** ## Связанные страницы # POST /v1/wallet Source: https://docs.oblodai.com/reference/wallet-create Создать (или получить существующий) постоянный статический адрес приёма. Средства на него зачисляются напрямую на доступный баланс мерчанта. **URL:** `https://api.oblodai.com/v1/wallet` · **Аутентификация:** обязательна · **Идемпотентность:** заголовок `Idempotency-Key` или тройка `(currency, network, order_id)`. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). ### Заголовки Любое уникальное значение (≤255 символов), одинаковое во всех повторах. Повтор вернёт тот же ответ и заголовок `Idempotent-Replayed: true`. → [Идемпотентность](/reference/basics-idempotency) *** ## Параметры запроса Символ валюты приёма (`USDT`, `BTC`, `ETH`, …). Сеть приёма (`tron`, `ethereum`, `bitcoin`, …). Ваш идентификатор клиента/заказа. Закрепляет отдельный постоянный адрес за клиентом. Пара `(currency, network)` должна быть принимаемым и отслеживаемым методом из [каталога](/reference/basics-networks). *** ## Пример запроса ```bash cURL theme={null} BODY='{"currency":"USDT","network":"tron","order_id":"client-42"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/wallet' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/wallet \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/wallet", {"currency": "USDT", "network": "tron", "order_id": "client-42"}) ``` ```js Node.js theme={null} await call("/v1/wallet", { currency: "USDT", network: "tron", order_id: "client-42" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "uuid": "0e5b6b9a-6a1e-4b7e-9d2b-2c1f4a8e9c11", "address": "TXk9...c3Fd", "network": "tron", "currency": "USDT", "order_id": "client-42", "url": "" } } ``` Полное описание полей — [Объект кошелька](/reference/wallet-object). *** ## Коды ошибок | Код | Значение | | -------------------------------- | ----------------------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 wallet.unknown_currency` | Неизвестная валюта. | | `400 wallet.no_network` | Не указана сеть. | | `400 wallet.unsupported_network` | Пара валюта+сеть не поддерживается. | *** ## Нюансы * **Идемпотентность по `(currency, network, order_id)`.** Для каждого клиента передавайте уникальный `order_id`, чтобы получить его персональный постоянный адрес. Повтор с той же тройкой вернёт тот же кошелёк. * **Комиссия платформы удерживается и с депозитов на статический кошелёк** — по ставке мерчанта (по умолчанию \~1.5 %). На баланс идёт нетто; поле `payment_amount` в вебхуке `wallet.paid` — это брутто (что пришло на адрес), до удержания комиссии. * Каждое поступление шлёт вебхук [`wallet.paid`](/reference/webhook-object). *** ## Связанные страницы # Объект кошелька (Wallet) Source: https://docs.oblodai.com/reference/wallet-object Статический кошелёк — это **постоянный адрес приёма**, закреплённый за мерчантом (и, по желанию, за конкретным клиентом через `order_id`). В отличие от инвойса, у него нет фиксированной суммы и срока: любое поступление сразу зачисляется на доступный баланс мерчанта. *** ## Чем статический кошелёк отличается от инвойса | | Инвойс ([`/v1/payment`](/reference/payment-create)) | Статический кошелёк ([`/v1/wallet`](/reference/wallet-create)) | | ------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Назначение | Разовый счёт на фиксированную сумму. | Постоянный адрес пополнения. | | Сумма | Задаётся заранее. | Не задаётся — принимается любая. | | Срок жизни | Ограничен (`lifetime`). | Бессрочный (до блокировки). | | Что происходит с оплатой | Закрывает счёт. | Зачисляется на **доступный баланс** мерчанта. | | Тип вебхука | `payment` (событие `invoice.*`). | `wallet` (событие **`wallet.paid`**). | | Комиссия платформы | Удерживается. | **Удерживается** — по вашей ставке (по умолч. \~1.5 %); на баланс идёт нетто, а `payment_amount` в вебхуке — брутто. | | Идемпотентность | по `order_id`. | по тройке `(currency, network, order_id)`. | Поскольку тройка `(currency, network, order_id)` идемпотентна, один `order_id` удобно закрепить за клиентом как его **персональный адрес пополнения** — см. [Статические кошельки](/guides/static-wallets). **Статические кошельки не экономят на комиссии** — она удерживается по вашей ставке (по умолч. \~1.5 %), как и на инвойсе. Кошельки — для пополнений/депозитов (напр. балансы пользователей), а не для разовых продаж товаров. Для продаж используйте инвойс ([`/v1/payment`](/reference/payment-create)) — он даёт сумму, срок и статус конкретного заказа. *** ## Пример объекта (ответ создания) ```json theme={null} { "uuid": "0e5b6b9a-6a1e-4b7e-9d2b-2c1f4a8e9c11", "address": "TXk9...c3Fd", "network": "tron", "currency": "USDT", "order_id": "client-42", "url": "" } ``` Идентификатор кошелька. Нужен для [возврата](/reference/wallet-blocked-refund). Постоянный адрес приёма. Сеть. Валюта. Ваш идентификатор клиента/заказа. Зарезервировано (обычно пусто). *** ## Вебхук пополнения Каждое поступление на статический кошелёк порождает вебхук типа `wallet` с событием `wallet.paid`: ```json theme={null} { "type": "wallet", "uuid": "0e5b6b9a-6a1e-4b7e-9d2b-2c1f4a8e9c11", "order_id": "client-42", "address": "TXk9...c3Fd", "network": "tron", "currency": "USDT", "payment_amount": "150.00", "payer_currency": "USDT", "txid": "9b1f...e4", "status": "paid", "is_final": true } ``` Формат заголовков и проверка подписи — [Объект вебхука](/reference/webhook-object). *** ## Связанные страницы создать кошелёк. заблокировать. вернуть средства. сценарий использования. # POST /v1/wallet/qr Source: https://docs.oblodai.com/reference/wallet-qr Рендерит произвольный адрес в QR‑код (`data:`‑URI). Кодируется присланная строка как есть — привязки к счёту или кошельку нет. **URL:** `https://api.oblodai.com/v1/wallet/qr` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). *** ## Параметры запроса Адрес (или любая строка) для кодирования в QR. *** ## Пример запроса ```bash cURL theme={null} BODY='{"address":"TXk9...c3Fd"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/wallet/qr' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/wallet/qr \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/wallet/qr", {"address": "TXk9...c3Fd"}) ``` ```js Node.js theme={null} await call("/v1/wallet/qr", { address: "TXk9...c3Fd" }); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "image": "data:image/png;base64,…" } } ``` *** ## Коды ошибок | Код | Значение | | ---------------------- | --------------------- | | `400 request.bad_json` | Тело не парсится. | | `400 qr.no_address` | Не передан `address`. | *** ## Нюансы * При ошибке генерации `image` вернётся **пустой строкой** — эндпоинт не падает. * Для QR депозит‑адреса конкретного счёта используйте [`POST /v1/payment/qr`](/reference/payment-qr). *** ## Связанные страницы # Объект вебхука и проверка подписи Source: https://docs.oblodai.com/reference/webhook-object Вебхуки — это HTTP‑уведомления, которые Oblodai шлёт на ваш URL, когда с платежом, кошельком или выплатой что‑то происходит. Боевые вебхуки шлёт фоновый диспетчер (не синхронно с вашим API‑вызовом). **Подпись вебхука ≠ подпись запроса.** Это два разных алгоритма. Подпись запроса описана в [Аутентификации](/reference/basics-auth); подпись вебхука — здесь. Не переиспользуйте один код для обоих. *** ## Гарантия доставки Доставка идёт по паттерну transactional‑outbox: строка доставки пишется в **той же транзакции**, что и зачисление платежа — уведомление не потеряется. Доставка — **как минимум один раз** (at‑least‑once). Успех = ответ `2xx`; любой не‑2xx или сетевая ошибка считается провалом, и доставка переносится с ретраем. *** ## Заголовки доставки | Заголовок | Значение | | --------------------- | ----------------------------------------------------------------- | | `Content-Type` | `application/json` | | `X-Webhook-Event` | Тип события: `invoice.paid`, `payout.confirmed`, `wallet.paid`, … | | `X-Webhook-Timestamp` | unix‑секунды момента отправки | | `X-Webhook-Signature` | `hex(HMAC-SHA256(secret, "." + сырое_тело))` | Где `secret` — тот, что вернул [`POST /v1/webhooks`](/reference/webhooks-register). *** ## Как считается подпись вебхука ``` signing_string = "{X-Webhook-Timestamp}" + "." + сырое_тело_запроса X-Webhook-Signature = hex( HMAC_SHA256( secret, signing_string ) ) ``` То есть: `timestamp` + точка `.` + **сырое тело** как есть. Никаких метода, пути или переводов строк — в отличие от подписи запроса. *** ## Типы событий (`X-Webhook-Event`) | Событие | Когда | Поле `type` в теле | | ---------------------- | --------------------------------------------------------------- | ------------------ | | `invoice.paid` | Инвойс оплачен в пределах допуска. | `payment` | | `invoice.paid_over` | Переплата сверх допуска. | `payment` | | `invoice.wrong_amount` | Недоплата, срок вышел. | `payment` | | `invoice.expired` | Счёт истёк. | `payment` | | `wallet.paid` | Пополнение статик‑кошелька. | `wallet` | | `payout.<статус>` | Событие выплаты; суффикс — **внутренний** статус (список ниже). | `payout` | Возможные события выплат: `payout.pending`, `payout.approved`, `payout.awaiting_cosign` (при 2‑of‑2), `payout.broadcasting`, `payout.sent`, `payout.confirmed` (успех), `payout.failed`, `payout.cancelled`. Подтверждённая выплата — `payout.confirmed`, **не** `payout.paid` (подробнее — в заметке к [примеру вебхука выплаты](#пример-тела-вебхука-выплаты-payoutстатус) ниже). *** У [выплатных ссылок (чеков)](/reference/payout-link) собственных событий нет: когда получатель забирает средства, порождённая выплата шлёт обычные `payout.*` события, а её `order_id` равен `payoutlink:` — по нему сопоставляйте событие со ссылкой. Возврат из [`/v1/payment/resolve`](/reference/payment-resolve) тоже приходит событиями `payout.*`. ## Пример тела платёжного вебхука ```json theme={null} { "type": "payment", "uuid": "3f1c…-e2b1", "order_id": "order-1001", "amount": "10.00", "currency": "USDT", "payment_amount": "10.00", "payer_amount": "10.00", "payer_currency": "USDT", "status": "paid", "is_final": true, "txid": "", "additional_data": "…" } ``` *** ## Пример тела вебхука пополнения кошелька (`wallet.paid`) Приходит при поступлении средств на [статический кошелёк](/reference/wallet-object). Поле `type` — `wallet`. ```json theme={null} { "type": "wallet", "uuid": "0e5b6b9a-…", "order_id": "user-42", "address": "TJ4b1…C9xk", "network": "tron", "currency": "USDT", "payment_amount": "149.50", "payer_currency": "USDT", "txid": "9f2c…a1", "status": "paid", "is_final": true } ``` *** ## Пример тела вебхука выплаты (`payout.<статус>`) Приходит при смене статуса [выплаты](/reference/payout-object). Поле `type` — `payout`. ```json theme={null} { "type": "payout", "uuid": "7d1e…-b3", "order_id": "wd-1001", "amount": "50.00", "currency": "USDT", "network": "tron", "address": "TYr2…9kQp", "txid": "3a1f…c7", "status": "paid", "is_final": true } ``` **Статус в заголовке и в теле — разного «уровня».** В заголовке `X-Webhook-Event` суффикс — **внутренний** статус (`payout.confirmed`, `payout.sent`, …), а поле `status` в **теле** — **укрупнённый** (`check` / `process` / `paid` / `fail` / `cancel`). Подтверждённая выплата придёт как `X-Webhook-Event: payout.confirmed` с `"status": "paid"` в теле. Ветвитесь по тому, что вам удобнее, но не ждите `payout.paid` в заголовке — такого события нет. См. [статусы выплаты](/reference/payout-object#статусы-выплаты). *** ## Проверка подписи ```python Python theme={null} import hmac, hashlib def verify(secret: bytes, ts_header: str, raw_body: bytes, sig_header: str) -> bool: msg = ts_header.encode() + b"." + raw_body expected = hmac.new(secret, msg, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, sig_header) ``` ```js Node.js theme={null} import crypto from "node:crypto"; function verify(secret, tsHeader, rawBody, sigHeader) { const msg = `${tsHeader}.` + rawBody; // rawBody — строка сырого тела const expected = crypto.createHmac("sha256", secret).update(msg).digest("hex"); if (expected.length !== sigHeader.length) return false; return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader)); } ``` ```php PHP theme={null} **Берите именно сырое тело запроса** (raw body), до любого JSON‑парсинга/пересериализации. Если тело переупаковать, байты изменятся и подпись не сойдётся. *** ## Ретраи и dead‑letter Backoff экспоненциальный: от 10 секунд с удвоением, потолок 1 час; всего до **12 попыток** (в сумме \~3,5 часа). Исчерпав попытки, доставка получает статус `dead` — она видна в [`POST /v1/webhooks/deliveries`](/reference/webhooks-deliveries). *** ## Идемпотентность на вашей стороне (обязательно) Из‑за at‑least‑once один вебхук может прийти **несколько раз** (в том числе после [`payment/resend`](/reference/payment-resend)). Правила: * **Дедуплицируйте** по паре `uuid` + `status` и обрабатывайте повтор как no‑op. * **Не полагайтесь на порядок** доставок — он не гарантирован. Опирайтесь на `status`/`is_final`, а не на очерёдность прихода. * **Отвечайте `2xx` только после успешной обработки** — иначе диспетчер повторит доставку. *** ## Связанные страницы регистрация URL и получение `secret`. журнал доставок. пошагово. # POST /v1/webhooks/deliveries Source: https://docs.oblodai.com/reference/webhooks-deliveries Журнал последних доставок вебхуков (до 50) — для отладки. **URL:** `https://api.oblodai.com/v1/webhooks/deliveries` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Тело:** пустой объект `{}` — параметров нет. *** ## Пример запроса ```bash cURL theme={null} TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/webhooks/deliveries' '{}' \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/webhooks/deliveries \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d '{}' ``` ```python Python theme={null} call("/v1/webhooks/deliveries", {}) ``` ```js Node.js theme={null} await call("/v1/webhooks/deliveries", {}); ``` *** ## Пример ответа ```json theme={null} { "state": 0, "result": { "deliveries": [ { "id": "9a2b…", "url": "https://shop.example/…", "event_type": "invoice.paid", "status": "delivered", "attempts": 1, "last_error": "", "created_at": "2026-07-10T08:15:04Z", "updated_at": "2026-07-10T08:15:05Z" } ] } } ``` Идентификатор доставки. Куда доставлялось. Тип события. Статус доставки (см. ниже). Сколько попыток сделано. Текст последней неуспешной попытки. Момент создания доставки (RFC 3339). Момент последнего обновления (RFC 3339). ### Статусы доставки | Статус | Значение | | ----------- | -------------------------- | | `pending` | В очереди или ждёт ретрая. | | `delivered` | Endpoint вернул `2xx`. | | `dead` | Исчерпаны попытки. | *** ## Нюансы * В журнале до **50** последних доставок. * Статус `dead` означает, что вебхук так и не был доставлен за все попытки. Для платёжных событий вебхук можно переотправить через [`POST /v1/payment/resend`](/reference/payment-resend). *** ## Связанные страницы # POST /v1/webhooks Source: https://docs.oblodai.com/reference/webhooks-register Регистрирует (или заменяет) URL проекта для вебхуков и возвращает `secret` для проверки их подписи. У проекта **один** активный endpoint — повторный вызов заменяет URL и **выдаёт новый секрет**. **URL:** `https://api.oblodai.com/v1/webhooks` · **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Исключение из формата ответа.** Этот метод возвращает `201 Created` и «голый» объект — **без** конверта `state`/`result`. Единственный такой эндпоинт в API. *** ## Параметры запроса HTTPS‑URL коллбэка. SSRF‑проверка: приватные и локальные адреса запрещены. *** ## Пример запроса ```bash cURL theme={null} BODY='{"url":"https://shop.example/oblodai/callback"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/webhooks' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/webhooks \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/webhooks", {"url": "https://shop.example/oblodai/callback"}) ``` ```js Node.js theme={null} await call("/v1/webhooks", { url: "https://shop.example/oblodai/callback" }); ``` *** ## Пример ответа (`201 Created`, без конверта) ```json theme={null} { "endpoint_id": "3f1c…-e2b1", "url": "https://shop.example/oblodai/callback", "secret": "b7c1e9…" } ``` Идентификатор endpoint. Зарегистрированный URL. Секрет для проверки подписи вебхуков. **Показывается один раз.** *** ## Коды ошибок | Код | Значение | | --------------------- | ------------------------------------------------------------- | | `400 webhook.no_url` | Не передан `url`. | | `400 webhook.bad_url` | Некорректный URL или запрещённый (приватный/локальный) адрес. | *** ## Нюансы * **`secret` показывается один раз** — сохраните его. Он нужен для проверки подписи по [алгоритму вебхука](/reference/webhook-object). * **Один endpoint на проект.** Повторный вызов заменяет URL и выдаёт **новый** секрет (старый перестаёт подходить). * **Per‑объектный `url_callback`** конкретного платежа/выплаты доставляется на свой URL, но **подписывается секретом endpoint проекта**. Значит, override работает, только когда endpoint зарегистрирован. *** ## Связанные страницы # Тестовые вебхуки Source: https://docs.oblodai.com/reference/webhooks-test Набор методов, чтобы отправить пробное уведомление на ваш URL и проверить приёмник, не дожидаясь реальной оплаты. **Аутентификация:** обязательна. Примеры используют хелпер `call()` и переменные `$SECRET`/`$PUBLIC_ID` — их определение см. в [Как подписать запрос](/guides/signing-requests). Проще не писать подпись руками, а взять [SDK](/sdk/overview). **Пробные тела содержат `"is_test": true`.** Отправка на явный `url`/`url_callback` НЕ подписывается (к произвольному URL не привязан секрет). Исключение — `/v1/payment/testing-webhook` **без `url`**: пробное тело уходит на зарегистрированный endpoint проекта и подписано его настоящим секретом (стандартные заголовки `X-Webhook-*`), так что можно проверить и свою [проверку подписи](/reference/webhook-object). *** ## POST /v1/payment/testing-webhook Синхронно шлёт пробное тело и возвращает HTTP‑код и время ответа вашего эндпоинта. Куда отправить пробное тело. **Не передан** — доставка уходит на зарегистрированный [endpoint проекта](/reference/webhooks-register), подписанная его секретом; если endpoint не зарегистрирован, вернётся ошибка `webhook.no_endpoint`. Статус в теле. По умолчанию `paid`. Ответ: ```json theme={null} { "state": 0, "result": { "result": true, "url": "https://shop.example/hook", "signed": true, "duration_ms": 42, "status_code": 200 } } ``` Если ваш эндпоинт не ответил, метод возвращает не ошибку, а результат пробы: ```json theme={null} { "state": 0, "result": { "result": false, "url": "https://shop.example/hook", "signed": true, "duration_ms": 10004, "error": "..." } } ``` *** ## POST /v1/test-webhook/payment · /wallet · /payout Пробный вебхук заданного типа на `url_callback`. Куда отправить пробное тело. Валюта в теле. Сеть в теле. UUID объекта (платежа, кошелька или выплаты), который попадёт в пробное тело события. Ваш `order_id`, который попадёт в пробное тело события. Статус в теле. Ответ: ```json theme={null} { "state": 0, "result": { "result": true, "status_code": 200 } } ``` `status_code` — HTTP‑код, которым ответил ваш эндпоинт. *** ## Пример запроса ```bash cURL wrap theme={null} BODY='{"url_callback":"https://shop.example/oblodai/callback","status":"paid","currency":"USDT","network":"tron"}' TS=$(date +%s) SIG=$(printf '%s\n%s\n%s\n%s' "$TS" 'POST' '/v1/test-webhook/payment' "$BODY" \ | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //') curl -s https://api.oblodai.com/v1/test-webhook/payment \ -X POST -H 'Content-Type: application/json' \ -H "X-Public-Id: $PUBLIC_ID" -H "X-Timestamp: $TS" -H "X-Signature: $SIG" \ -d "$BODY" ``` ```python Python theme={null} call("/v1/test-webhook/payment", { "url_callback": "https://shop.example/oblodai/callback", "status": "paid", "currency": "USDT", "network": "tron", }) ``` ```js Node.js theme={null} await call("/v1/test-webhook/payment", { url_callback: "https://shop.example/oblodai/callback", status: "paid", currency: "USDT", network: "tron", }); ``` *** ## Коды ошибок | Код | Значение | | ------------------------- | ------------------------------------ | | `400 webhook.no_url` | Не передан `url` / `url_callback`. | | `400 webhook.bad_url` | Некорректный URL. | | `503 webhook.test_failed` | Ваш эндпоинт не принял пробное тело. | *** ## Нюансы * Пробные тела **не подписаны** и помечены `"is_test": true`. Ваш код проверки подписи должен уметь отличать их (или тестируйте подпись отдельно на боевых событиях). * `testing-webhook` — легаси‑форма; для новых интеграций предпочитайте `test-webhook/*`. *** ## Связанные страницы # Баланс мерчанта Source: https://docs.oblodai.com/api-reference/баланс-и-курсы/баланс-мерчанта /api-reference/openapi.json post /v1/balance Ваши доступные балансы по каждой валюте. Тело — пустой `{}`. # Курсы обмена к USDT Source: https://docs.oblodai.com/api-reference/баланс-и-курсы/курсы-обмена-к-usdt /api-reference/openapi.json post /v1/exchange-rate/list Список курсов. Необязательный `currency_from` фильтрует по исходной валюте. # Журнал доставок вебхуков Source: https://docs.oblodai.com/api-reference/вебхуки/журнал-доставок-вебхуков /api-reference/openapi.json post /v1/webhooks/deliveries Последние доставки: URL, статус, число попыток, последняя ошибка — для отладки. # Зарегистрировать endpoint для коллбэков Source: https://docs.oblodai.com/api-reference/вебхуки/зарегистрировать-endpoint-для-коллбэков /api-reference/openapi.json post /v1/webhooks Задаёт URL проекта, куда слать вебхуки, и возвращает `secret` (показывается один раз) для проверки подписи `X-Webhook-Signature`. # Перевыпустить секрет подписи вебхуков Source: https://docs.oblodai.com/api-reference/вебхуки/перевыпустить-секрет-подписи-вебхуков /api-reference/openapi.json post /v1/webhooks/rotate-secret Единственный момент, когда новый секрет показывается. До `previous_secret_valid_until` доставки дополнительно несут `X-Webhook-Signature-Prev` со старым секретом — время докатить замену без потери проверки. # Переотправить вебхук по платежу Source: https://docs.oblodai.com/api-reference/вебхуки/переотправить-вебхук-по-платежу /api-reference/openapi.json post /v1/payment/resend Заново поставит в очередь коллбэк по платежу (по `uuid`/`order_id`). Полезно, если ваш сервер был недоступен. # Тестовый вебхук ВЫПЛАТЫ Source: https://docs.oblodai.com/api-reference/вебхуки/тестовый-вебхук-выплаты /api-reference/openapi.json post /v1/test-webhook/payout Доставит пробный вебхук типа payout. # Тестовый вебхук КОШЕЛЬКА Source: https://docs.oblodai.com/api-reference/вебхуки/тестовый-вебхук-кошелька /api-reference/openapi.json post /v1/test-webhook/wallet Доставит пробный вебхук типа wallet (пополнение статик-кошелька). # Тестовый вебхук на URL (старый вариант) Source: https://docs.oblodai.com/api-reference/вебхуки/тестовый-вебхук-на-url-старый-вариант /api-reference/openapi.json post /v1/payment/testing-webhook Шлёт пробное тело на указанный `url` — проверить, что ваш обработчик работает. # Тестовый вебхук ПЛАТЕЖА Source: https://docs.oblodai.com/api-reference/вебхуки/тестовый-вебхук-платежа /api-reference/openapi.json post /v1/test-webhook/payment Доставит пробный вебхук типа payment на `url_callback`. # Вернуть платёж Source: https://docs.oblodai.com/api-reference/возвраты/вернуть-платёж /api-reference/openapi.json post /v1/payment/refund Возврат — это списание с вашего баланса. `address` (куда вернуть) можно опустить ТОЛЬКО если в платеже `payer_address_is_refundable` = true: тогда вернём на записанный адрес плательщика (`payer_address`). Если там false — адрес плательщика нам известен, но он не является адресом возврата (Bitcoin/UTXO: первый вход мог быть биржей или сдачей; XRP: общий адрес биржи с тегом назначения; оплата КАРТОЙ через крипто-он-рамп: отправитель — омнибусный горячий кошелёк провайдера, а не покупатель). Возврат туда уходит безвозвратно тому, кто денег не платил, поэтому запрос без `address` будет отклонён (`refund.no_address`): спросите адрес у покупателя и передайте его явно. Нужен `uuid`/`order_id` платежа. По умолчанию вернём всю полученную сумму; можно указать частичную `amount`. Идемпотентно по `(платёж, адрес, сумма)`; суммарно нельзя вернуть больше, чем оплачено. Возврат на записанный адрес плательщика подтверждается автоматически — кроме платежей картой через он-рамп, где такой возврат уходит в обычную очередь подтверждения. # Вернуть средства со статик-кошелька Source: https://docs.oblodai.com/api-reference/возвраты/вернуть-средства-со-статик-кошелька /api-reference/openapi.json post /v1/wallet/blocked-address-refund Возвращает на `address` ЧИСТУЮ сумму, полученную на (заблокированном) статик-кошельке: из полученного вычитается уже возвращённое. Пока возврат жив (создан, отправлен, подтверждён), повторный вызов возвращает его же. Если возврат не состоялся (failed/cancelled), вызов можно повторить — в том числе на другой адрес. Отменённые reorg'ом депозиты не считаются. # Отменить выплатную ссылку Source: https://docs.oblodai.com/api-reference/выплатные-ссылки/отменить-выплатную-ссылку /api-reference/openapi.json post /v1/payout/link/cancel Непогашенная ссылка отменяется, резерв возвращается на баланс. # Получить выплату по ссылке (без ключа) Source: https://docs.oblodai.com/api-reference/выплатные-ссылки/получить-выплату-по-ссылке-без-ключа /api-reference/openapi.json post /v1/claim/{token} Получатель вводит свой `address` (и `memo`, если сеть требует) — из резерва рождается обычная выплата. # Создать выплатную ссылку Source: https://docs.oblodai.com/api-reference/выплатные-ссылки/создать-выплатную-ссылку /api-reference/openapi.json post /v1/payout/link Резервирует сумму с баланса и выпускает ссылку, по которой получатель сам вводит адрес и забирает деньги. Адрес получателя знать не нужно. `email` — отправим письмо со ссылкой; `expires_in_hours` — окно на получение (1–720, по умолчанию 720). Идемпотентность: `reference` (или заголовок `Idempotency-Key`). # Создать выплатные ссылки пачкой Source: https://docs.oblodai.com/api-reference/выплатные-ссылки/создать-выплатные-ссылки-пачкой /api-reference/openapi.json post /v1/payout/link/batch До 500 ссылок за вызов; каждая проходит или падает независимо, ответ выровнен по индексам запроса. Повтор с теми же `reference` безопасен. # Список выплатных ссылок Source: https://docs.oblodai.com/api-reference/выплатные-ссылки/список-выплатных-ссылок /api-reference/openapi.json post /v1/payout/link/list # Статус выплатной ссылки Source: https://docs.oblodai.com/api-reference/выплатные-ссылки/статус-выплатной-ссылки /api-reference/openapi.json post /v1/payout/link/info # Страница получения: что внутри ссылки (без ключа) Source: https://docs.oblodai.com/api-reference/выплатные-ссылки/страница-получения:-что-внутри-ссылки-без-ключа /api-reference/openapi.json get /v1/claim/{token} Публичный просмотр для получателя: валюта, сумма, заметка, срок. Токен — секрет из URL письма. # Внутренний перевод пользователю платформы Source: https://docs.oblodai.com/api-reference/выплаты/внутренний-перевод-пользователю-платформы /api-reference/openapi.json post /v1/transfer/to-user Перевести средства с бизнес-кошелька на личный кошелёк ДРУГОГО пользователя платформы (без комиссии, мгновенно, без сети). Получатель адресуется по user id; юзернейм резолвится публичным эндпоинтом кабинета /public/users/{username}. # Доступные валюты и сети для выплат Source: https://docs.oblodai.com/api-reference/выплаты/доступные-валюты-и-сети-для-выплат /api-reference/openapi.json post /v1/payout/services Список с лимитами и комиссиями. Тело — пустой `{}`. # История выплат Source: https://docs.oblodai.com/api-reference/выплаты/история-выплат /api-reference/openapi.json post /v1/payout/history Список ваших выплат с пагинацией и фильтром по датам. # Массовая выплата Source: https://docs.oblodai.com/api-reference/выплаты/массовая-выплата /api-reference/openapi.json post /v1/payout/mass Много выплат за один запрос (до 100). Каждая независима: ошибка по одной не останавливает остальные, по каждой возвращается результат. Идемпотентно по `order_id`, как обычная выплата. # Массовые внутренние переводы (ведомость) Source: https://docs.oblodai.com/api-reference/выплаты/массовые-внутренние-переводы-ведомость /api-reference/openapi.json post /v1/transfer/batch Асинхронная пачка внутренних переводов: {"transfers":[<как /v1/transfer/to-user>...], "on_error":"continue"}. Статус и результаты по строкам — POST /v1/batch/info. # Перевод на личный кошелёк Source: https://docs.oblodai.com/api-reference/выплаты/перевод-на-личный-кошелёк /api-reference/openapi.json post /v1/transfer/to-personal Перевести средства с бизнес-кошелька мерчанта на личный кошелёк владельца аккаунта. Требует привязки мерчанта к пользователю. # Подтвердить выплату Source: https://docs.oblodai.com/api-reference/выплаты/подтвердить-выплату /api-reference/openapi.json post /v1/payout/approve Подтверждает выплату, ожидающую подтверждения. Выплаты по API-ключу подтверждаются автоматически — этот метод нужен только внутренним/кабинетным сценариям. # Рассчитать сумму и комиссию выплаты Source: https://docs.oblodai.com/api-reference/выплаты/рассчитать-сумму-и-комиссию-выплаты /api-reference/openapi.json post /v1/payout/calculate Предварительный расчёт: сколько спишется, сколько комиссия, сколько получит адрес — без создания выплаты. # Создать выплату Source: https://docs.oblodai.com/api-reference/выплаты/создать-выплату /api-reference/openapi.json post /v1/payout Отправить деньги на адрес. Идемпотентно по `order_id`. Адреса не из белого списка ждут ручного подтверждения (`approval_required: true`) — подтвердите через `/v1/payout/approve`. **Конвертация (`from_currency`):** укажите `from_currency: "USDT"`, чтобы оплатить выплату в `currency`, списав ваш баланс USDT — мы сконвертируем USDT → `currency` (только те валюты, что казначейство может добыть он-чейн). В ответе появится объект `convert` с `from_amount` (сколько USDT списано) и `rate`. Ещё: `memo` (тег/мемо для TON), `url_callback` (свой адрес вебхука для этой выплаты). # Узнать статус выплаты Source: https://docs.oblodai.com/api-reference/выплаты/узнать-статус-выплаты /api-reference/openapi.json post /v1/payout/info По `uuid`/`order_id`. # Вкл/выкл IP-allowlist Source: https://docs.oblodai.com/api-reference/ключи-и-безопасность/вклвыкл-ip-allowlist /api-reference/openapi.json post /v1/api-allowlist/enable Когда включён — запросы с IP не из списка отклоняются. # Добавить IP в allowlist Source: https://docs.oblodai.com/api-reference/ключи-и-безопасность/добавить-ip-в-allowlist /api-reference/openapi.json post /v1/api-allowlist/add # Список разрешённых IP Source: https://docs.oblodai.com/api-reference/ключи-и-безопасность/список-разрешённых-ip /api-reference/openapi.json post /v1/api-allowlist/list # Удалить IP из allowlist Source: https://docs.oblodai.com/api-reference/ключи-и-безопасность/удалить-ip-из-allowlist /api-reference/openapi.json post /v1/api-allowlist/remove # QR-код адреса Source: https://docs.oblodai.com/api-reference/кошельки/qr-код-адреса /api-reference/openapi.json post /v1/wallet/qr Возвращает PNG data:-URI по полю `address` — для ``. # Заблокировать / разблокировать кошелёк Source: https://docs.oblodai.com/api-reference/кошельки/заблокировать-разблокировать-кошелёк /api-reference/openapi.json post /v1/wallet/block Заблокированный кошелёк перестаёт зачислять новые пополнения. `is_force_block` по умолчанию true (блокировать); передайте false, чтобы снять блокировку. # Создать (или получить) статический кошелёк Source: https://docs.oblodai.com/api-reference/кошельки/создать-или-получить-статический-кошелёк /api-reference/openapi.json post /v1/wallet Постоянный адрес пополнения, закреплённый за мерчантом (и, по желанию, за одним клиентом через `order_id`). Любое пополнение на него сразу падает вам на баланс + шлёт вебхук. Идемпотентно по `(currency, network, order_id)`: тот же `order_id` вернёт тот же адрес — удобно закрепить адрес за каждым клиентом. # Массовое создание платежей Source: https://docs.oblodai.com/api-reference/массовые-операции/массовое-создание-платежей /api-reference/openapi.json post /v1/payment/batch До 5000 платежей за ОДИН запрос (одна отметка rate-limit). Каждый элемент — обычный объект `/v1/payment` (разные валюты/сети допустимы). В ответ сразу приходит `batch_id`; обработка идёт в фоне. Статус и результаты (включая `uuid` и ссылку оплаты каждого платежа) — через `/v1/batch/info`. `on_error`: `continue` (по умолчанию — ошибка одного не мешает остальным) или `stop` (после первой ошибки оставшиеся отменяются). Каждый элемент идемпотентен по своему `order_id`; вся пачка — по заголовку `Idempotency-Key`. # Массовые возвраты Source: https://docs.oblodai.com/api-reference/массовые-операции/массовые-возвраты /api-reference/openapi.json post /v1/refund/batch До 5000 возвратов за один запрос. Каждый элемент — обычный объект `/v1/payment/refund`, но `reference` ОБЯЗАТЕЛЕН на каждом элементе и уникален внутри батча: это ключ идемпотентности именно этого возврата (не путать с `order_id`, который указывает на счёт). Без него два разных возврата одной суммы одному плательщику молча схлопнулись бы в один. Возвращает `batch_id`; статус по каждому — через `/v1/batch/info`. `on_error`: `continue`/`stop`. # Массовые выплаты (async, без лимита 100) Source: https://docs.oblodai.com/api-reference/массовые-операции/массовые-выплаты-async-без-лимита-100 /api-reference/openapi.json post /v1/payout/batch Асинхронный аналог `/v1/payout/mass` без ограничения в 100: до 5000 выплат, обработка в фоне, статус через `/v1/batch/info`. Каждый элемент — обычный объект `/v1/payout`, идемпотентен по `order_id`. # Статус пачки Source: https://docs.oblodai.com/api-reference/массовые-операции/статус-пачки /api-reference/openapi.json post /v1/batch/info Прогресс пачки (`total`/`succeeded`/`failed`/`status`) и постранично её элементы с результатом или ошибкой по каждому. `status`: `pending` → `processing` → `completed`. # Авто-конверт волатильных монет в USDT (VRCS) Source: https://docs.oblodai.com/api-reference/настройки/авто-конверт-волатильных-монет-в-usdt-vrcs /api-reference/openapi.json post /v1/vrcs Включает автоматическую конвертацию поступающих волатильных монет в стейбл USDT. # Кто платит нашу комиссию при возврате Source: https://docs.oblodai.com/api-reference/настройки/кто-платит-нашу-комиссию-при-возврате /api-reference/openapi.json post /v1/payout/refund-fee-config/set `fee_on_customer: true` — при возврате нашу комиссию несёт клиент (возврат за вычетом комиссии); false — несёт мерчант. # Кто платит сетевую комиссию выплаты Source: https://docs.oblodai.com/api-reference/настройки/кто-платит-сетевую-комиссию-выплаты /api-reference/openapi.json post /v1/payout/fee-config/set `fee_on_recipient: true` — комиссию сети платит получатель (ему приходит сумма минус комиссия). # Настроить авто-вывод Source: https://docs.oblodai.com/api-reference/настройки/настроить-авто-вывод /api-reference/openapi.json post /v1/auto-withdraw/set Автоматически выводить поступления на заданный адрес. # Настроить автовозвраты Source: https://docs.oblodai.com/api-reference/настройки/настроить-автовозвраты /api-reference/openapi.json post /v1/payment/autorefund/set `overpay` — авто-возврат излишка переплаты; `underpay` — авто-возврат при истёкшей недоплате. Оба по умолчанию ВКЛ. Возврат идёт на адрес плательщика (EVM/Tron/TON/Solana; на Bitcoin/UTXO — вручную). # Настроить допуск недо/переплаты Source: https://docs.oblodai.com/api-reference/настройки/настроить-допуск-недопереплаты /api-reference/openapi.json post /v1/payment/accuracy/set «Точность платежей»: `enabled` + `accuracy_percent` 1–5. В пределах допуска платёж считается оплаченным. Выключено — нужна точная сумма. # Настроить принимаемые валюты магазина Source: https://docs.oblodai.com/api-reference/настройки/настроить-принимаемые-валюты-магазина /api-reference/openapi.json post /v1/payment/accepted/set Задаёт, какие валюты/сети магазин принимает. # Прочитать допуск сумм Source: https://docs.oblodai.com/api-reference/настройки/прочитать-допуск-сумм /api-reference/openapi.json post /v1/payment/accuracy/get # Прочитать настройку автовозвратов Source: https://docs.oblodai.com/api-reference/настройки/прочитать-настройку-автовозвратов /api-reference/openapi.json post /v1/payment/autorefund/get # Прочитать настройку комиссии возврата Source: https://docs.oblodai.com/api-reference/настройки/прочитать-настройку-комиссии-возврата /api-reference/openapi.json post /v1/payout/refund-fee-config/get # Прочитать настройку комиссии выплат Source: https://docs.oblodai.com/api-reference/настройки/прочитать-настройку-комиссии-выплат /api-reference/openapi.json post /v1/payout/fee-config/get # Скидка/наценка на способ оплаты Source: https://docs.oblodai.com/api-reference/настройки/скидканаценка-на-способ-оплаты /api-reference/openapi.json post /v1/payment/discount/set Положительный `discount_percent` — скидка плательщику за оплату этой монетой; отрицательный — наценка. # Список правил авто-вывода Source: https://docs.oblodai.com/api-reference/настройки/список-правил-авто-вывода /api-reference/openapi.json post /v1/auto-withdraw/list # Список принимаемых валют Source: https://docs.oblodai.com/api-reference/настройки/список-принимаемых-валют /api-reference/openapi.json post /v1/payment/accepted/list # Список скидок/наценок Source: https://docs.oblodai.com/api-reference/настройки/список-скидокнаценок /api-reference/openapi.json post /v1/payment/discount/list # Удалить правило авто-вывода Source: https://docs.oblodai.com/api-reference/настройки/удалить-правило-авто-вывода /api-reference/openapi.json post /v1/auto-withdraw/delete # QR-код адреса оплаты Source: https://docs.oblodai.com/api-reference/оформление-без-ключа/qr-код-адреса-оплаты /api-reference/openapi.json get /v1/pay/{id}/qr PNG-картинка с QR того адреса (и суммы), которые уже вернул `GET /v1/pay/{id}`. Без ключа — её грузит браузер покупателя. # Выбрать валюту и сеть для валюто-агностичной ссылки Source: https://docs.oblodai.com/api-reference/оформление-без-ключа/выбрать-валюту-и-сеть-для-валюто-агностичной-ссылки /api-reference/openapi.json post /v1/pay/{id}/select Клиент выбирает `currency` + `network`; после этого фиксируется курс и выделяется адрес. # Конфиг платёжной ссылки (для страницы) Source: https://docs.oblodai.com/api-reference/оформление-без-ключа/конфиг-платёжной-ссылки-для-страницы /api-reference/openapi.json get /v1/link/{id} Публично: заголовок/описание/режим суммы/валюта — чтобы отрисовать страницу доната. # Оплатить по ссылке (создать платёж) Source: https://docs.oblodai.com/api-reference/оформление-без-ключа/оплатить-по-ссылке-создать-платёж /api-reference/openapi.json post /v1/link/{id}/checkout Публично: клиент вводит сумму (для open/range) и, если валюта не закреплена, выбирает валюту/сеть. Создаётся свежий инвойс — в ответе обычный объект платежа с `uuid` и `url` страницы оплаты. # Публичный статус платежа (страница оплаты) Source: https://docs.oblodai.com/api-reference/оформление-без-ключа/публичный-статус-платежа-страница-оплаты /api-reference/openapi.json get /v1/pay/{id} Без секрета — можно опрашивать прямо из браузера. Содержит `amount_remaining` для подсказки «доплатите X». # Список валют и сетей Source: https://docs.oblodai.com/api-reference/оформление-без-ключа/список-валют-и-сетей /api-reference/openapi.json get /v1/currencies Публичный справочник. Возвращает два списка, и путать их не надо: - `currencies` — в чём можно **получать**: монеты и их сети (плюс флаги доступности приёма и выплаты). - `pricing_currencies` — в чём можно **назначать цену** (`currency` при создании платежа): те же монеты **плюс 45 фиатных валют** (`{"symbol":"EUR","decimals":2,"fiat":true}`) — USD, EUR, GBP, RUB, UAH, PLN, CZK, TRY, CNY, INR, BRL, CAD, AUD, CHF, AED, ZAR, MXN, IDR, THB, VND, NGN, JPY, KRW, SGD, HKD, NZD, SEK, NOK, DKK, ILS, SAR, PHP, MYR, TWD, PKR, LKR, MMK, BDT, ARS, GEL, HUF, BMD, BHD, KWD, CLP. Число знаков после запятой у каждой в поле `decimals` (обычно 2; у JPY/KRW/VND/CLP — 0, у BHD/KWD — 3) — берите его из ответа, не хардкодьте. У фиата нет сетей и никогда не будет: в нём можно оценить счёт, но нельзя его получить. Тенге, сом и сум пока не поддерживаются — источник курсов не котирует в них крипту напрямую, а выводить курс перемножением двух других мы не будем. # Журнал доставок вебхуков dev-store Source: https://docs.oblodai.com/api-reference/песочница/журнал-доставок-вебхуков-dev-store /api-reference/openapi.json get /v1/sandbox/webhooks # Кран: пополнить тестовый баланс Source: https://docs.oblodai.com/api-reference/песочница/кран:-пополнить-тестовый-баланс /api-reference/openapi.json post /v1/sandbox/faucet Только для тестового ключа dev-store. Начисляет тестовые деньги, чтобы гонять выплаты/возвраты, а не только приём. # Переотправить доставку вебхука Source: https://docs.oblodai.com/api-reference/песочница/переотправить-доставку-вебхука /api-reference/openapi.json post /v1/sandbox/webhooks/replay Ставит доставку заново в очередь настоящего диспетчера — с его ретраями и подписью, как в проде. # Сбросить dev-store к чистому состоянию Source: https://docs.oblodai.com/api-reference/песочница/сбросить-dev-store-к-чистому-состоянию /api-reference/openapi.json post /v1/sandbox/reset # Симулировать он-чейн депозит Source: https://docs.oblodai.com/api-reference/песочница/симулировать-он-чейн-депозит /api-reference/openapi.json post /v1/sandbox/deposit Проводит синтетический платёж через настоящий пайплайн зачисления. `amount` пустой — оплатить ровно сколько нужно; `confirmations` меньше требуемого — проверка перехода pending→confirmed (повторите тот же `txid` с большим числом); тот же `txid` повторно — проверка вашей идемпотентности. # Создать (или вернуть) dev-store мерчанта Source: https://docs.oblodai.com/api-reference/песочница/создать-или-вернуть-dev-store-мерчанта /api-reference/openapi.json post /v1/merchants/{id}/sandbox Идемпотентно: у мерчанта максимум один dev-store, повторный вызов возвращает существующий. Тестовый ключ возвращается каждый раз — он не защищает ничего, кроме тестовых денег. Вызывается под онбординг-гейтом кабинета, не HMAC-ключом. # Разрешить недоплату: принять или вернуть Source: https://docs.oblodai.com/api-reference/платежи/разрешить-недоплату:-принять-или-вернуть /api-reference/openapi.json post /v1/payment/resolve Для платежа в статусе `wrong_amount` (недоплата, срок вышел) мерчант явно решает судьбу денег: `action:"accept"` — оставить частичную оплату как расчёт (снимает автовозврат), `action:"refund"` — вернуть полученное плательщику сейчас (адрес/сеть по умолчанию — записанный адрес плательщика). Двигает деньги, поэтому подписывается ключом выплат. # Включить/выключить ссылку Source: https://docs.oblodai.com/api-reference/платёжные-ссылки/включитьвыключить-ссылку /api-reference/openapi.json post /v1/payment/link/toggle `{link_id, active}`. Выключенная ссылка не принимает новые платежи. # Создать платёжную ссылку Source: https://docs.oblodai.com/api-reference/платёжные-ссылки/создать-платёжную-ссылку /api-reference/openapi.json post /v1/payment/link Переиспользуемая ссылка (как страница доната): по ней платят много людей, каждый платёж — свой инвойс со своим адресом. `amount_mode`: `fixed` (сумма задана в `amount_fixed`), `open` (клиент вводит любую сумму, опц. `amount_min`), `range` (клиент вводит в диапазоне `amount_min`…`amount_max`). `currency` — валюта цены (крипто-тикер, напр. `USDT`). Валюту/сеть оплаты можно **закрепить** (`pinned_currency` + `pinned_network`) или оставить пустыми — тогда клиент выбирает их на странице оплаты. `expires_in` — срок жизни ссылки в секундах (0 = **бессрочно**; сами инвойсы при этом живут обычный короткий срок). В ответе — `link_id` и `url` для клиента. # Список ссылок Source: https://docs.oblodai.com/api-reference/платёжные-ссылки/список-ссылок /api-reference/openapi.json post /v1/payment/link/list Ваши платёжные ссылки, новые сверху. # Ссылка + её платежи Source: https://docs.oblodai.com/api-reference/платёжные-ссылки/ссылка-+-её-платежи /api-reference/openapi.json post /v1/payment/link/info По `link_id`: конфиг ссылки и собранные по ней платежи (`payments[]`). # Реферальная информация Source: https://docs.oblodai.com/api-reference/рефералы/реферальная-информация /api-reference/openapi.json post /v1/referral/info Ваш реферальный код, приглашённые и начисления. # Окно удержания под возвраты Source: https://docs.oblodai.com/api-reference/сплит-платежи/окно-удержания-под-возвраты /api-reference/openapi.json post /v1/split/config/set `refund_hold_hours` — на сколько часов откладывается ВСЯ исходящая маршрутизация платежа (сплиты партнёрам, авто-вывод, авто-конвертация в USDT) после его зачисления. Смысл: пока окно не истекло, деньги лежат на вашем балансе, и любой возврат проходит без проблем. `0` = отправлять сразу — тогда риск возврата после отправки вы берёте на себя. Максимум 2160 (90 дней). # Правило сплита (отчисление партнёру) Source: https://docs.oblodai.com/api-reference/сплит-платежи/правило-сплита-отчисление-партнёру /api-reference/openapi.json post /v1/split/rule Автоматически отправлять долю КАЖДОГО входящего платежа партнёру. Укажите ровно одного получателя: • `address` + `network` — внешний крипто-адрес. Уходит он-чейн выплатой, **необратимо**. • `merchant_id` — аккаунт на Oblodai. Уходит проводкой по балансу: **обратимо** (возврат отзовёт долю обратно). `percent` — доля от платежа (напр. `10` или `2.5`). Сумма всех активных правил проекта не может превышать 100%. ⚠️ **Возвраты.** Возврат списывается с ВАШЕГО баланса на всю сумму, что прислал плательщик. Поэтому отправка партнёрам не происходит сразу: она откладывается на `refund_hold_hours` (см. `/v1/split/config/set`), и в момент отправки база пересчитывается как «оплачено − возвращено». Возврат внутри окна автоматически уменьшает (или отменяет) отчисление, и вам всегда есть чем вернуть деньги. Возврат ПОСЛЕ отправки: внешнюю долю вернуть нельзя (пополняйте баланс), долю on-platform партнёра мы отзовём автоматически. # Список правил Source: https://docs.oblodai.com/api-reference/сплит-платежи/список-правил /api-reference/openapi.json post /v1/split/rule/list Ваши правила сплита. `reversible: true` — партнёр на платформе (долю можно отозвать при возврате). # Текущее окно удержания Source: https://docs.oblodai.com/api-reference/сплит-платежи/текущее-окно-удержания /api-reference/openapi.json post /v1/split/config/get Возвращает `refund_hold_hours` проекта. # Удалить правило Source: https://docs.oblodai.com/api-reference/сплит-платежи/удалить-правило /api-reference/openapi.json post /v1/split/rule/delete `{rule_id}`. На уже отправленные доли не влияет. # Go SDK Source: https://docs.oblodai.com/sdk/go Официальная библиотека для Go. Модуль `github.com/oblodai/oblodai-go`. Ноль внешних зависимостей (только стандартная библиотека). **Требования:** Go **1.22.2+** (именно эта версия указана в `go.mod` — на 1.22.0/1.22.1 сборка не пройдёт). *** ## Установка ```bash theme={null} go get github.com/oblodai/oblodai-go ``` **Новое в v1.1.0** (текущая версия в Go modules): группы `Batches` / `Links` / `Splits` / `PayoutLinks`, методы `*Batch` / `SendEmail` / `Resolve` — и **ломающее изменение** идемпотентности: вместо авто-`order_id` теперь заголовок `Idempotency-Key` (см. «Ошибки и повторы»). *** ## Аутентификация (ключи из окружения) Ключи берутся в кабинете [my.oblodai.com](https://my.oblodai.com) — см. [Регистрация и ключи](/guides/get-keys). ```bash theme={null} export OBLODAI_PUBLIC_ID=oblodai_ваш_public_id export OBLODAI_SECRET=oblodai_ваш_secret # необязательно: export OBLODAI_BASE_URL=https://api.oblodai.com ``` ```go theme={null} import oblodai "github.com/oblodai/oblodai-go" // читает OBLODAI_PUBLIC_ID / OBLODAI_SECRET / OBLODAI_BASE_URL client, err := oblodai.NewFromEnv(oblodai.Config{}) ``` Или явно: ```go theme={null} client, err := oblodai.New(oblodai.Config{ PublicID: "...", Secret: "...", BaseURL: "https://api.oblodai.com", }) ``` *** ## Быстрый старт: принять платёж ```go theme={null} payment, err := client.Payments.Create(context.Background(), oblodai.Params{ "amount": "10", "currency": "USD", "order_id": "order-1", "to_currency": "USDT", "network": "tron", "url_callback": "https://ваш-сайт.ру/oblodai/webhook", }) if err != nil { /* обработать */ } http.Redirect(w, r, payment.URL, http.StatusFound) // на страницу оплаты ``` `oblodai.Params` — это `map[string]any`. Метод возвращает типизированный объект (`*oblodai.Payment`). Не хотите указывать валюту/сеть за покупателя? Не передавайте `to_currency` и `network` — получится «валюто-агностичная» ссылка, где покупатель сам выберет монету на странице оплаты. *** ## Ресурсы и методы | Группа | Методы | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client.Payments` | `Create`, `Info`, `History`, `Services`, `QR`, `Resend`, `Refund`, `ListAccepted`, `SetAccepted`, `GetAccuracy`, `SetAccuracy`, `GetAutorefund`, `SetAutorefund`, `SetDiscount`, `ListDiscounts` · **с v1.1.0:** `CreateBatch`, `RefundBatch`, `SendEmail`, `Resolve` | | `client.Payouts` | `Create`, `CreateMass`, `Info`, `History`, `Services`, `Calculate`, `Approve`, `GetFeeConfig`, `SetFeeConfig`, `GetRefundFeeConfig`, `SetRefundFeeConfig`, `Refund` · **с v1.1.0:** `CreateBatch` | | `client.Batches` **(v1.1.0)** | `Info` | | `client.Links` **(v1.1.0)** | `Create`, `List`, `Info`, `Toggle`, `PublicGet`, `Checkout` | | `client.PayoutLinks` **(v1.1.0)** | `Create`, `CreateBatch` (до 500), `List`, `Info`, `Cancel` + публичные без подписи: `ClaimInfo(ctx, token)`, `Claim(ctx, token, address)`, `ClaimWithMemo(ctx, token, address, memo)` | | `client.Splits` **(v1.1.0)** | `CreateRule`, `SplitToAddress`, `SplitToMerchant`, `ListRules`, `DeleteRule`, `GetConfig`, `SetConfig` | | `client.Wallets` | `Create`, `Block`, `BlockedAddressRefund`, `QR` | | `client.Account` | `Balance`, `Referral`, `TransferToPersonal`, `VRCS` | | `client.Webhooks` | `Register`, `Deliveries`, `TestPayment`, `TestWallet`, `TestPayout` | | `client.Settings` | `ListAutoWithdraw`, `SetAutoWithdraw`, `DeleteAutoWithdraw`, `ListAllowlist`, `AddAllowlist`, `RemoveAllowlist`, `EnableAllowlist` | | `client.Rates` | `List` (курсы), `Currencies` (публичный каталог) | Все методы принимают `context.Context` первым аргументом. Точные поля — в [Справочнике](/reference/overview). Всё, что помечено «с v1.1.0», доступно начиная с версии **1.1.0** — если у вас стоит v1.0.x, обновите модуль (`go get -u github.com/oblodai/oblodai-go`). Свой ключ идемпотентности в методах создания — `params["idempotency_key"]` (уйдёт в заголовок). ```go theme={null} // Массовые операции: до 5000 элементов одним подписанным запросом (одна отметка rate-limit). sub, err := client.Payments.CreateBatch(ctx, []oblodai.Params{ {"amount": "10", "currency": "USD", "order_id": "a-1", "to_currency": "USDT", "network": "tron"}, {"amount": "20", "currency": "EUR", "order_id": "a-2", "to_currency": "USDT", "network": "tron"}, }, "continue") // "continue" (по умолчанию) или "stop" info, err := client.Batches.Info(ctx, sub.BatchID, 100, 0) // прогресс и результат по каждому элементу // Платёжная ссылка: платят многие, каждый платёж — свой инвойс. Принимает деньги без вашего бэкенда. link, err := client.Links.Create(ctx, oblodai.LinkParams{AmountMode: "open", Currency: "USD"}) // Сплит: доля каждого входящего платежа автоматически уходит партнёру. rule, err := client.Splits.SplitToAddress(ctx, "T...", "tron", 10.0, "партнёр А") // Счёт на e-mail (письмо с кнопкой «Оплатить»). _, err = client.Payments.SendEmail(ctx, payment.UUID, "" /* orderID */, "buyer@example.com") // Резолв недоплаты: принять частичную оплату (глушит авто-возврат) или вернуть плательщику. res, err := client.Payments.Resolve(ctx, payment.UUID, "" /* orderID */, "accept", nil) // или "refund" ``` **Крипто-чек за 4 строки** — выплата без адреса получателя (заберёт сам по ссылке): ```go theme={null} check, err := client.PayoutLinks.Create(ctx, oblodai.PayoutLinkParams{ Amount: "25", Currency: "USDT", Network: "tron", Reference: "bonus-42", ExpiresInHours: 72, Email: "winner@example.com", }) fmt.Println(check.ClaimURL) // отдайте получателю — он введёт свой адрес сам ``` * Задавайте `ExpiresInHours` **явно**: без него чек живёт всего **1 час**. * `ClaimURL` / `ClaimToken` возвращаются **только из `Create`, один раз** — сохраните сразу. * Дедупликация — поле `Reference` (заголовок `Idempotency-Key` на этих эндпоинтах не действует). Подробности — [Массовые операции](/guides/batch-operations), [Платёжные ссылки](/guides/payment-links), [Сплит-платежи](/guides/split-payments), [Счета на e-mail](/guides/email-invoices), [Крипто-чеки](/guides/payout-links), [Резолв платежа](/reference/payment-resolve). *** ## Валюта цены и валюта расчёта В примере выше `currency: "USD"` — это **валюта цены**, а `to_currency: "USDT"` — **валюта расчёта**. Их постоянно путают: * **`currency` (цена)** — одна из **23 фиатных валют**: USD, EUR, GBP, RUB, UAH, PLN, CZK, TRY, CNY, INR, BRL, CAD, AUD, CHF, AED, ZAR, MXN, IDR, THB, VND, NGN, JPY, KRW — **или любая монета** (USDT, BTC, TRX, …). * **`to_currency` (расчёт)** — **только крипта.** Фиата здесь не бывает: баланс, выплаты и возвраты всегда в монете, шлюз не хранит фиат. * У **JPY и KRW ноль знаков** после запятой (`"amount": "10000"`, не `"10000.00"`); у остальных — 2. * **KZT, KGS, UZS пока не поддерживаются** → `payment.unknown_currency`. * Фиатная цена + **только** `network` без `to_currency` → `400 payment.to_currency_required`. Либо задайте `to_currency`, либо не задавайте **ни то, ни другое** (тогда монету выберет покупатель). Полный список валют цены — `client.Rates.Currencies(ctx)`, поле `pricing_currencies`. *** ## Проверьте, что заработало 1. Запустите код создания платежа из «Быстрого старта». В ответе должен прийти `URL` — это ссылка на hosted-страницу оплаты. Откройте её в браузере: если страница открылась, приём платежей настроен. 2. Чтобы реально **поймать вебхук** об оплате на локальной машине, нужен публичный HTTPS-адрес — поднимите туннель (ngrok / cloudflared) и укажите его URL в `url_callback`. Подробнее — в [Тестировании](/guides/testing) и [Настройке вебхуков](/guides/webhooks-setup). *** ## Вебхуки ```go Шаг 1. Регистрация (один раз) theme={null} ep, err := client.Webhooks.Register(ctx, "https://ваш-сайт.ру/oblodai/webhook") // сохраните ep.Secret — им проверяются вебхуки (не API-секрет!) ``` ```go Шаг 2. Приём (обязательно СЫРОЕ тело) theme={null} func webhook(w http.ResponseWriter, r *http.Request) { raw, _ := io.ReadAll(r.Body) var ev map[string]any json.Unmarshal(raw, &ev) if ev["is_test"] == true { w.WriteHeader(http.StatusOK); return } // пробные тела не подписаны headers := oblodai.WebhookHeaders{ Timestamp: r.Header.Get("X-Webhook-Timestamp"), Signature: r.Header.Get("X-Webhook-Signature"), } if err := oblodai.VerifyWebhook(webhookSecret, raw, headers, nil); err != nil { w.WriteHeader(http.StatusForbidden); return } info, _ := client.Payments.Info(r.Context(), ev["uuid"].(string), "") if info.PaymentStatus == "paid" || info.PaymentStatus == "paid_over" { // пометить заказ info.OrderID оплаченным (идемпотентно) } w.WriteHeader(http.StatusOK) } ``` `VerifyWebhook` возвращает `*oblodai.SignatureError` при неудаче; проверяет подпись и окно replay. Тестовые вебхуки (`is_test`) не подписаны — их можно просто подтверждать 200. *** ## Ошибки и повторы ```go theme={null} var apiErr *oblodai.APIError if errors.As(err, &apiErr) { apiErr.Code // "payout.insufficient_funds" — ветвитесь по коду apiErr.Status // HTTP-статус apiErr.IsRetriable() } ``` Клиент **сам** повторяет 5xx/429/сетевые сбои с backoff и учётом `Retry-After` (до 4 попыток) — **повторы включены по умолчанию**, настраивать ничего не нужно. Пустое поле `Retry` (`nil`) означает «дефолтные повторы», как и в остальных четырёх SDK. Если повторы нужно **отключить**, сделайте это явно: ```go theme={null} client, err := oblodai.New(oblodai.Config{ PublicID: "...", Secret: "...", Retry: oblodai.NoRetry(), // отключить повторы // oblodai.DefaultRetry() — то же, что и nil; передавать не обязательно }) ``` **Это изменилось.** В прежних версиях `Retry: nil` означало «повторов НЕТ» — то есть клиент, созданный самым очевидным способом, молча оставался без повторов и без учёта `Retry-After` на 429. Теперь поведение такое же, как у остальных SDK. **Повтор безопасен, но механизм зависит от версии:** | | **v1.0.x** (предыдущая) | **v1.1.0** (текущая, в Go modules) | | ---------------- | --------------------------------------------------- | ------------------------------------------------------------------- | | Защита от дублей | `order_id`: не задали — **SDK подставит свой** | заголовок `Idempotency-Key`, одинаковый во всех внутренних повторах | | Ваш `order_id` | если не задан — в платеже будет сгенерированный SDK | уходит **как есть**, SDK не подставляет и не переписывает | | Свой ключ | `idempotency_key` **не существует** | можно передать `idempotency_key` — уйдёт в заголовок | В обеих версиях таймаут/обрыв сети не создаст дубль счёта или перевода. Для **выплат** `order_id` обязателен всегда — его задаёте вы (иначе `payout.order_id_required`). *** ## Логи и отладка Включите логи одной переменной окружения — увидите каждый запрос/ответ/ретрай: ```bash theme={null} export OBLODAI_LOG=debug # уровни: debug · info · warn · error ``` Строки идут в stderr с префиксом `oblodai:` (метод, путь, статус, номер попытки, задержка ретрая). **Секреты, подпись и тела запросов в лог не попадают.** Вместо переменной можно задать свой логгер: `Config{ Logger: slog-логгер }` — учтите, что `slog.Default()` работает на уровне INFO и debug-строки запросов не покажет; задайте debug-хендлер или `OBLODAI_LOG=debug`. **Проверяете подпись вебхука статичным (старым) вектором?** По умолчанию действует окно свежести 5 минут — старый `timestamp` отклонит replay-защита (валидная подпись → всё равно ошибка). Для офлайн-проверки задайте окно 0: `oblodai.VerifyWebhook(secret, raw, headers, &oblodai.VerifyOptions{MaxAgeSeconds: 0})`. *** ## Если не получилось | Симптом | Причина и что делать | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `*APIError` `401` на каждый вызов | Не заданы `OBLODAI_PUBLIC_ID`/`OBLODAI_SECRET` или перепутаны. SDK подписывает сам. | | `NewFromEnv` возвращает ошибку | Обязательная переменная окружения не задана (её имя — в тексте ошибки). | | Вебхук не проходит проверку | Читайте **сырое** тело (`io.ReadAll(r.Body)` до парсинга) и **webhook‑секрет** из `Webhooks.Register()`, не API‑секрет. | | Вебхук вообще не приходит | Не зарегистрировали endpoint (`Webhooks.Register(ctx, url)`) или сайт недоступен из интернета. | | `429` | SDK повторяет сам с учётом `Retry-After` (повторы включены по умолчанию — если вы не задали `NoRetry()`). Не поллите `Payments.Info()` в цикле. Много операций — шлите их [пачкой](/guides/batch-operations). | Полная диагностика — [Что делать, если не работает](/guides/troubleshooting). ## Связанные страницы # SDK — библиотеки для вашего языка Source: https://docs.oblodai.com/sdk/overview Библиотеки для PHP, TypeScript, Python, Go и Rust. SDK — это готовая библиотека, которая берёт на себя всю «рутину» интеграции с Oblodai: подписывает запросы, разбирает ответы, проверяет подписи вебхуков, повторяет запросы при сбоях. Вам остаётся вызвать пару методов. **Новичку:** если вы пишете свой сайт/бэкенд — берите SDK, а не «голый» HTTP. Так вы не ошибётесь в подписи запроса (самый частый источник проблем на старте). *** ## Какой SDK выбрать Все SDK устроены одинаково — одни и те же понятия и почти одинаковые названия методов. Освоив один, вы сразу поймёте остальные. `composer require oblodai/sdk` `npm install @oblodai-npm/sdk` `pip install oblodai` `go get github.com/oblodai/oblodai-go` `cargo add oblodai` **v1.1.0 — текущая версия в реестрах** (Packagist · npm · PyPI · Go modules · crates.io). Ломающее изменение против v1.0.x: идемпотентность переехала с авто-`order_id` на заголовок `Idempotency-Key` — см. принцип 3 и CHANGELOG вашего SDK. npm-пакет: `@oblodai-npm/sdk`. Чтобы устанавливать пакеты, понадобятся сами инструменты вашего языка: [Composer](https://getcomposer.org/download/) (PHP), [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (Node.js), [pip](https://pip.pypa.io/en/stable/installation/) (Python), [Go](https://go.dev/doc/install) (go), [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) (Rust). *** ## Что вам понадобится (для любого SDK) 1. **`public_id` и `secret`** — пара ключей вашего мерчанта. `public_id` — несекретный идентификатор, `secret` — тайна, которой подписываются запросы. Один ключ на весь функционал (и приём, и выплаты). Ключи берутся в кабинете [my.oblodai.com](https://my.oblodai.com) — см. [Регистрация и ключи](/guides/get-keys). 2. **Сервер.** SDK работает **только на сервере** (бэкенд). Секрет никогда не должен попадать в браузер или мобильное приложение. 3. **Публичный URL для вебхуков** — адрес на вашем сервере, куда Oblodai будет присылать уведомления об оплате (например `https://ваш-сайт.ру/oblodai/webhook`). *** ## Шесть принципов за минуту Эти правила одинаковы во всех SDK — понимание их избавит от 90 % ошибок. 1. **Ключи — из переменных окружения.** Не пишите секрет в коде. Задайте `OBLODAI_PUBLIC_ID` и `OBLODAI_SECRET`, и создавайте клиента через `fromEnv()` / `from_env()`. Базовый URL по умолчанию — боевой `https://api.oblodai.com`; переопределить можно переменной `OBLODAI_BASE_URL` (необязательно). 2. **Суммы — строками.** `"25.00"`, не число `25.0`. Так не теряется точность. 3. **Повтор безопасен — но защита от дублей устроена по-разному в двух версиях.** Это главное отличие v1.1.0 от v1.0.x. | | **v1.0.x** — предыдущая версия | **v1.1.0** — текущая версия | | ------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Что защищает от дублей | `order_id`: если вы его **не задали, SDK подставлял свой** | Заголовок `Idempotency-Key`: генерируется один раз на вызов и **одинаков во всех внутренних повторах** | | Ваш `order_id` | если не задан — в платеже окажется **сгенерированный SDK** | уходит **как есть**; SDK ничего не подставляет и не переписывает | | Свой ключ идемпотентности | параметра `idempotency_key` **не существует** | передайте `idempotency_key` в вызов создания — уйдёт в **заголовок**, не в тело | * **Ловушка при обновлении на v1.1.0.** Если вы полагались на то, что `order_id` «появится сам» в ответе, — теперь его там **не будет**. Задавайте `order_id` **явно** (см. следующий раздел). Сама защита от дублей при этом работает и без него — уже заголовком. * **`order_id` — это ВАШ бизнес-идентификатор**, по которому вы потом находите платёж через `payments.info` и сопоставляете со своим заказом. Ключом идемпотентности он быть перестал. * **Выплаты:** `order_id` **обязателен в обеих версиях** — этого требует сам API (без него вернётся `payout.order_id_required`). Задавайте свой уникальный номер выплаты. * **Ходите в API напрямую, без SDK?** Тогда шлите заголовок `Idempotency-Key` сами (или полагайтесь на `order_id`) — иначе повтор задублирует операцию. Подробно — [Идемпотентность](/reference/basics-idempotency). 4. **Оплату подтверждает вебхук, а не редирект.** Покупателя редиректит на «спасибо», но статус заказа меняйте **только** получив вебхук (и перепроверив статус через `payments.info`). 5. **Вебхуки надо зарегистрировать.** Один раз вызовите «регистрацию вебхука» — получите webhook-секрет и храните его. Именно им проверяются входящие уведомления (это НЕ ваш API-секрет). Без регистрации уведомления не приходят. 6. **Валюта цены — это не валюта расчёта.** Два разных поля, и их постоянно путают: * **`currency` — валюта ЦЕНЫ.** Может быть одной из **23 фиатных валют** — USD, EUR, GBP, RUB, UAH, PLN, CZK, TRY, CNY, INR, BRL, CAD, AUD, CHF, AED, ZAR, MXN, IDR, THB, VND, NGN, JPY, KRW — **или любой монетой** (USDT, BTC, TRX, …). Указать цену в рублях или евро можно, и это надёжно: курс монеты приходит сразу в нужной валюте, ничего не перемножается. * **`to_currency` — валюта РАСЧЁТА.** Здесь **только крипта, фиата не бывает никогда.** Баланс, выплаты и возвраты — тоже всегда в крипте: шлюз не хранит фиат. * У **JPY и KRW ноль знаков после запятой**: `"amount": "10000"`, а не `"10000.00"`. У остальных — 2. * **Тенге (KZT), сом (KGS), сум (UZS) пока не поддерживаются** — вернётся `payment.unknown_currency`. * **Правило, о которое спотыкаются:** цена в фиате + задана только `network` **без** `to_currency` → `400 payment.to_currency_required`. Либо задайте `to_currency`, либо **не задавайте ни то, ни другое** — тогда получится валюто-агностичный счёт и монету выберет сам покупатель. Полный список того, в чём можно назначать цену, отдаёт `rates.currencies()` — в ответе поле `pricing_currencies` (монеты + фиат с признаком `"fiat": true`). *** ## Как связать платёж со своим заказом Всегда возвращается `uuid` платежа — по нему платёж ищется через `payments.info`. Но удобнее искать по **своему** `order_id`, поэтому задавайте его сами и **сохраняйте до вызова**: ```python Python theme={null} import uuid order_id = "ord-" + uuid.uuid4().hex db.save(order_id=order_id, status="pending") # сохранили ПЕРЕД отправкой payment = client.payments.create(amount="10", currency="USDT", order_id=order_id, network="tron") db.update(order_id, payment_uuid=payment.uuid) ``` ```ts Node.js theme={null} import { randomUUID } from "node:crypto"; const orderId = "ord-" + randomUUID(); await db.save({ orderId, status: "pending" }); // сохранили ПЕРЕД отправкой const payment = await client.payments.create({ amount: "10", currency: "USDT", order_id: orderId, network: "tron", }); await db.update(orderId, { paymentUuid: payment.uuid }); ``` **Почему «до вызова».** Если `create()` бросит исключение (сеть отвалилась после того, как SDK исчерпал повторы), ответа у вас не будет — и записать `uuid` будет неоткуда. Дубля при этом не возникнет (все повторы шли с одним `Idempotency-Key`), но чтобы потом **найти** этот платёж, вам нужен ключ, который вы придумали сами. Поэтому: сначала записали свой `order_id`, потом отправили. **Где взять `order_id` из ответа:** | Язык | Доступ | | ------- | ---------------------- | | TS/Node | `payment.order_id` | | Python | `payment.order_id` | | Rust | `payment.order_id` | | Go | `payment.OrderID` | | PHP | `$payment['order_id']` | *** ## Что нового в v1.1.0 * **Массовые операции.** До 5000 платежей / возвратов / выплат **одним** запросом — одна отметка rate-limit вместо тысячи. Обработка в фоне: постановка возвращает `batch_id`, результаты по каждому элементу забираются через «инфо о пачке». Есть режим `on_error`: `continue` (по умолчанию) или `stop` (остановиться на первой ошибке). См. [Массовые операции](/guides/batch-operations), [Пачка](/reference/payment-batch), [Инфо о пачке](/reference/batch-info). * **Платёжные ссылки (донаты).** Переиспользуемая ссылка: по ней платят много людей, каждый платёж — свой инвойс со своим адресом. Сумма фиксированная, свободная или в диапазоне; валюту и сеть можно закрепить или дать выбрать клиенту; ссылка может быть **бессрочной**. Это **единственный способ принимать платежи вообще без бэкенда** (Tilda, Wix и т. п.). См. [Платёжные ссылки](/guides/payment-links), [Ссылка](/reference/payment-link), [Публичный доступ к ссылке](/reference/link-public). * **Сплит-платежи.** Доля каждого платежа автоматически уходит партнёру — на внешний адрес (необратимо) или аккаунту на платформе (обратимо: возврат отзовёт долю). См. предупреждение о возвратах ниже, [Сплит-платежи](/guides/split-payments), [Правило сплита](/reference/split-rule), [Настройки сплита](/reference/split-config). * **Счёт на e-mail** — письмо покупателю с кнопкой «Оплатить». Чек об оплате уходит автоматически на `payer_email`, если вы задали его при создании платежа. См. [Счета на e-mail](/guides/email-invoices), [Отправка счёта](/reference/payment-send-email). * **Крипто-чеки (payout links)** — выплата **без адреса получателя**: вы резервируете сумму, получатель открывает `claim_url` и сам вводит свой адрес. Группа `payout_links` / `payoutLinks` / `PayoutLinks`: `create`, `create_batch` (до 500 ссылок), `list`, `info`, `cancel` + публичные `claim_info` / `claim` (без подписи). См. [Крипто-чеки](/guides/payout-links), [Payout-ссылка](/reference/payout-link), [Публичный claim](/reference/claim-public). * **Резолв недоплаты** — `payments.resolve(action=accept|refund)`: недоплаченный платёж можно принять как есть (`accept`, глушит авто-возврат) или вернуть плательщику (`refund`). См. [Резолв платежа](/reference/payment-resolve). * **Новые поля платежа:** `payer_address` (откуда пришли деньги), `refunds[]` и `refund_status` (`none|partial|full`). * **`address` в возврате больше не обязателен** — по умолчанию вернём на адрес плательщика. Для Bitcoin/UTXO, где адрес плательщика неизвестен, `address` по-прежнему нужен. ### Сплиты и возвраты — прочитайте, прежде чем включать Возврат списывается с **вашего** баланса на всю сумму, что прислал плательщик. Если бы доля партнёра уходила сразу, вам могло бы стать нечем возвращать. Поэтому отправка партнёрам **откладывается на окно удержания** (`refund_hold_hours`), и в момент отправки база пересчитывается как «оплачено − возвращено». Возврат внутри окна сам уменьшает или отменяет отчисление. Возврат **после** отправки: долю на внешний адрес вернуть нельзя (придётся пополнить баланс), а долю партнёра-на-платформе мы отзовём автоматически. *** ## Типичный сценарий приёма платежа Одинаков во всех языках: ``` 1. (однократно) Зарегистрировать вебхук → сохранить webhook-секрет 2. Покупатель нажал «Оплатить» 3. Ваш сервер: client.payments.create({ amount, currency, order_id, url_callback }) 4. Редиректите покупателя на payment.url (hosted-страница оплаты Oblodai) 5. Покупатель платит в крипте 6. Oblodai шлёт вебхук на ваш url_callback 7. Ваш сервер: проверяете подпись вебхука → payments.info(uuid) → если "paid", отмечаете заказ оплаченным ``` Пошагово с кодом — на странице вашего языка. Начните с [приёма первого платежа](/guides/accept-first-payment), если хотите сперва понять сам API. *** ## Логи (диагностика) Логирование во всех SDK **выключено по умолчанию** и включается одинаково — переменной окружения или своим логгером. Секреты (`secret`, `X-Signature`, webhook-секрет) и тела запросов **не логируются никогда** — только метод, путь, HTTP-статус, время, номер попытки, задержка ретрая и код ошибки. **Самый простой способ — переменная окружения** (значения `debug` · `info` · `warn` · `error`): ```bash theme={null} export OBLODAI_LOG=debug ``` Тогда в stderr пойдут понятные строки: ``` oblodai: -> POST /v1/payment (attempt 1/4) oblodai: <- 200 POST /v1/payment 123ms oblodai: retrying POST /v1/payment in 60000ms (429 rate limit; attempt 2/4) oblodai: webhook signature ok ``` **Свой логгер** (в прод — направьте в свою систему логов) задаётся в конфиге клиента: * **TypeScript** — `new OblodaiClient({ ..., logger: (level, msg, fields) => … })` * **Python** — стандартный `logging`: настройте логгер `logging.getLogger("oblodai")` * **Go** — `oblodai.Config{ ..., Logger: slog.Default() }` * **Rust** — `Config::new(...).logger(Arc::new(|level, msg| …))` * **PHP** — `new Client($publicId, $secret, ['logger' => function (string $level, string $msg) { … }])` *** ## Лимиты частоты и повторы (встроено) Все пять SDK **сами** переживают временные сбои, и повторы **включены по умолчанию** — отдельно настраивать ничего не нужно: * **Что повторяется:** `HTTP 429` (лимит частоты) и `5xx`, а также сетевые сбои/таймауты. Ошибки запроса (`4xx`, кроме 429) не повторяются. * **`Retry-After`:** на `429` шлюз присылает `Retry-After: 60` — SDK ждёт именно столько; иначе — экспоненциальный backoff с джиттером. * **Дефолты:** до 4 попыток, старт 500 мс, потолок задержки 30 с. Настраивается через `RetryConfig`/ `RetryOptions` (или отключается) в конфиге клиента. * Лимит самого API — **120 запросов/мин на IP**. Подробно — [Ограничение частоты](/reference/basics-ratelimit). **Go, внимание.** Раньше в Go-SDK повторов по умолчанию **не было** (`Retry: nil` означало «не повторять») — из пяти библиотек ошибалась одна. Это **исправлено**: теперь `nil` = повторы включены, как везде. Отключить их можно осознанно — `Retry: oblodai.NoRetry()`. **Упираетесь в лимит частоты?** Не разгоняйте параллелизм — используйте [массовые операции](/guides/batch-operations) (с v1.1.0): до 5000 элементов **одним** подписанным запросом = одна отметка rate-limit вместо тысяч. *** ## Куда дальше Выберите язык из карточек выше и откройте его страницу — там установка, рабочий пример и все методы. Точные поля каждого метода API. Незнакомый термин — смотрите здесь. Не пишете код — возьмите готовый модуль. # PHP SDK Source: https://docs.oblodai.com/sdk/php Официальная библиотека для PHP. Пакет `oblodai/sdk`. Без внешних зависимостей (работает на cURL), поддерживает подстановку своего HTTP-транспорта. **Требования:** PHP 8.0+, расширения `json` и `curl`. *** ## Установка ```bash theme={null} composer require oblodai/sdk ``` **Новое в v1.1.0** (текущая версия в Packagist): группы `batches()` / `links()` / `splits()` / `payoutLinks()`, методы `createBatch` / `refundBatch` / `sendEmail` / `resolve` — и **ломающее изменение** идемпотентности: вместо авто-`order_id` теперь заголовок `Idempotency-Key` (см. «Ошибки и повторы»). *** ## Аутентификация (ключи из окружения) Ключи берутся в кабинете [my.oblodai.com](https://my.oblodai.com) — см. [Регистрация и ключи](/guides/get-keys). Задайте переменные окружения (не пишите секрет в коде): ```bash theme={null} export OBLODAI_PUBLIC_ID=oblodai_ваш_public_id export OBLODAI_SECRET=oblodai_ваш_secret # необязательно: export OBLODAI_BASE_URL=https://api.oblodai.com ``` ```php theme={null} use Oblodai\Client; $client = Client::fromEnv(); // читает OBLODAI_PUBLIC_ID / OBLODAI_SECRET / OBLODAI_BASE_URL ``` Или явно: ```php theme={null} $client = new Oblodai\Client($publicId, $secret, [ 'base_url' => 'https://api.oblodai.com', // необязательно ]); ``` *** ## Быстрый старт: принять платёж ```php theme={null} use Oblodai\Client; $client = Client::fromEnv(); $payment = $client->payments()->create([ 'amount' => '10', 'currency' => 'USD', 'order_id' => 'order-1', // ваш ID заказа 'to_currency' => 'USDT', 'network' => 'tron', 'url_callback' => 'https://ваш-сайт.ру/oblodai/webhook', 'url_success' => 'https://ваш-сайт.ру/thanks', ]); // Отправьте покупателя оплачивать: header('Location: ' . $payment['url']); ``` Совет: не хотите указывать валюту/сеть за покупателя? Не передавайте `to_currency` и `network` — получится «валюто-агностичная» ссылка, где покупатель сам выберет монету на странице оплаты. *** ## Ресурсы и методы Доступ через `$client->имя()`: | Группа | Методы | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `payments()` | `create`, `info`, `history`, `services`, `qr`, `walletQr`, `resend`, `refund`, `listAccepted`, `setAccepted`, `getAccuracy`, `setAccuracy`, `getAutorefund`, `setAutorefund`, `listDiscounts`, `setDiscount` · **с v1.1.0:** `createBatch`, `refundBatch`, `sendEmail`, `resolve` | | `payouts()` | `create`, `createMass`, `info`, `history`, `services`, `calculate`, `approve`, `getFeeConfig`, `setFeeConfig`, `getRefundFeeConfig`, `setRefundFeeConfig`, `refund` · **с v1.1.0:** `createBatch` | | `batches()` **(v1.1.0)** | `info` | | `links()` **(v1.1.0)** | `create`, `list`, `info`, `toggle`, `publicGet`, `checkout` · `links()` = алиас `paymentLinks()` | | `payoutLinks()` **(v1.1.0)** | `create`, `createBatch` (до 500), `list`, `info`, `cancel` + публичные без подписи: `claimInfo($token)`, `claim($token, $address, $memo = null)` | | `splits()` **(v1.1.0)** | `splitToAddress`, `splitToMerchant`, `listRules`, `deleteRule`, `getConfig`, `setConfig` | | `wallets()` | `create`, `block`, `blockedAddressRefund`, `qr` | | `account()` | `balance`, `referral`, `transferToPersonal`, `vrcs` | | `webhooks()` | `register`, `deliveries`, `testPayment`, `testWallet`, `testPayout` | | `settings()` | `listAutoWithdraw`, `setAutoWithdraw`, `deleteAutoWithdraw`, `listAllowlist`, `addAllowlist`, `removeAllowlist`, `enableAllowlist` | | `rates()` | `list` (курсы), `currencies` (публичный каталог монет/сетей) | Каждый метод возвращает уже развёрнутый `result` (массив). Точные поля запроса/ответа — в [Справочнике](/reference/overview). Всё, что помечено «с v1.1.0», доступно начиная с версии **1.1.0** — если у вас стоит v1.0.x, обновите пакет (`composer update oblodai/sdk`). Свой ключ идемпотентности в методах создания — `'idempotency_key'` в массиве параметров (уйдёт в заголовок). ```php theme={null} // Массовые операции: до 5000 элементов одним подписанным запросом (одна отметка rate-limit). $sub = $client->payments()->createBatch([ ['amount' => '10', 'currency' => 'USD', 'order_id' => 'a-1', 'to_currency' => 'USDT', 'network' => 'tron'], ['amount' => '20', 'currency' => 'EUR', 'order_id' => 'a-2', 'to_currency' => 'USDT', 'network' => 'tron'], ], 'continue'); // 'continue' (по умолчанию) или 'stop' $info = $client->batches()->info($sub['batch_id'], 100, 0); // прогресс и результат по элементам // Платёжная ссылка: платят многие, каждый платёж — свой инвойс. Работает без вашего бэкенда. $link = $client->links()->create(['amount_mode' => 'open', 'currency' => 'USD']); // Сплит: доля каждого входящего платежа автоматически уходит партнёру. $client->splits()->splitToAddress('T...', 'tron', 10.0, 'партнёр А'); // Счёт на e-mail (письмо с кнопкой «Оплатить»). $client->payments()->sendEmail($payment['uuid'], null, 'buyer@example.com'); // Резолв недоплаты: принять частичную оплату (глушит авто-возврат) или вернуть плательщику. $client->payments()->resolve(['uuid' => $payment['uuid'], 'action' => 'accept']); // или 'refund' ``` **Крипто-чек за 4 строки** — выплата без адреса получателя (заберёт сам по ссылке): ```php theme={null} $check = $client->payoutLinks()->create([ 'amount' => '25', 'currency' => 'USDT', 'network' => 'tron', 'reference' => 'bonus-42', 'expires_in_hours' => 72, 'email' => 'winner@example.com', ]); echo $check['claim_url']; // отдайте получателю — он введёт свой адрес сам ``` * Задавайте `expires_in_hours` **явно**: без него чек живёт всего **1 час**. * `claim_url` / `claim_token` возвращаются **только из `create`, один раз** — сохраните сразу. * Дедупликация — поле `reference` (заголовок `Idempotency-Key` на этих эндпоинтах не действует). Подробности — [Массовые операции](/guides/batch-operations), [Платёжные ссылки](/guides/payment-links), [Сплит-платежи](/guides/split-payments), [Счета на e-mail](/guides/email-invoices), [Крипто-чеки](/guides/payout-links), [Резолв платежа](/reference/payment-resolve). *** ## Валюта цены и валюта расчёта В примере выше `'currency' => 'USD'` — **валюта цены**, а `'to_currency' => 'USDT'` — **валюта расчёта**. Это разные вещи: * **`currency` (цена)** — одна из **23 фиатных валют**: USD, EUR, GBP, RUB, UAH, PLN, CZK, TRY, CNY, INR, BRL, CAD, AUD, CHF, AED, ZAR, MXN, IDR, THB, VND, NGN, JPY, KRW — **или любая монета** (USDT, BTC, TRX, …). * **`to_currency` (расчёт)** — **только крипта.** Фиата здесь не бывает: баланс, выплаты и возвраты всегда в монете, шлюз не хранит фиат. * У **JPY и KRW ноль знаков** после запятой (`'amount' => '10000'`, не `'10000.00'`); у остальных — 2. * **KZT, KGS, UZS пока не поддерживаются** → `payment.unknown_currency`. * Фиатная цена + **только** `network` без `to_currency` → `400 payment.to_currency_required`. Либо задайте `to_currency`, либо не задавайте **ни то, ни другое** (тогда монету выберет покупатель). Полный список валют цены — `$client->rates()->currencies()`, поле `pricing_currencies`. *** ## Проверьте, что заработало 1. Запустите код создания платежа из «Быстрого старта». В ответе должен прийти `url` — это ссылка на hosted-страницу оплаты. Откройте её в браузере: если страница открылась, приём платежей настроен. 2. Чтобы реально **поймать вебхук** об оплате на локальной машине, нужен публичный HTTPS-адрес — поднимите туннель (ngrok / cloudflared) и укажите его URL в `url_callback`. Подробнее — в [Тестировании](/guides/testing) и [Настройке вебхуков](/guides/webhooks-setup). *** ## Вебхуки ```php Шаг 1. Регистрация (один раз) theme={null} $endpoint = $client->webhooks()->register('https://ваш-сайт.ру/oblodai/webhook'); // Сохраните $endpoint['secret'] в БД/конфиге — им проверяются ВСЕ вебхуки. ``` Важно: этот webhook-секрет — **не** ваш API-`secret`. Без регистрации Oblodai не будет слать уведомления. ```php Шаг 2. Приём (обязательно СЫРОЕ тело) theme={null} use Oblodai\Webhooks; use Oblodai\Exception\SignatureException; $raw = file_get_contents('php://input'); // Пробные вебхуки (кнопка «Тест» в кабинете) не подписаны — просто подтверждаем. if (Webhooks::isTest($raw)) { http_response_code(200); exit; } try { $event = Webhooks::constructEvent( $webhookSecret, // сохранённый на шаге 1 $raw, $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '' ); } catch (SignatureException $e) { http_response_code(403); exit; } // Перепроверяем статус авторитетно (тело вебхука само по себе не финально): $info = $client->payments()->info($event['uuid']); if (($info['payment_status'] ?? '') === 'paid' || ($info['payment_status'] ?? '') === 'paid_over') { // Пометить заказ $info['order_id'] оплаченным (идемпотентно!) } http_response_code(200); ``` Статусы `payment_status`: `check`, `confirm_check`, `paid`, `paid_over`, `wrong_amount`, `wrong_amount_waiting`, `cancel`, `select`. Оплаченными считайте только `paid` и `paid_over`. *** ## Ошибки и повторы ```php theme={null} use Oblodai\Exception\ApiException; use Oblodai\Exception\ConnectionException; try { $client->payouts()->create([...]); } catch (ApiException $e) { $e->getErrorCode(); // напр. "payout.insufficient_funds" — ветвитесь ПО КОДУ, не по тексту $e->getStatusCode(); // HTTP-статус $e->isRetriable(); // временная ли ошибка } catch (ConnectionException $e) { // сеть/таймаут — безопасно повторить: SDK сам подставляет ключ идемпотентности } ``` Клиент **сам** повторяет 5xx / 429 / сетевые сбои с экспоненциальной задержкой и учётом заголовка `Retry-After`. Отключить повторы: `new Client($id, $secret, ['retry' => false])`. **Повтор безопасен, но механизм зависит от версии:** | | **v1.0.x** (предыдущая) | **v1.1.0** (текущая, в Packagist) | | ---------------- | --------------------------------------------------- | ------------------------------------------------------------------- | | Защита от дублей | `order_id`: не задали — **SDK подставит свой** | заголовок `Idempotency-Key`, одинаковый во всех внутренних повторах | | Ваш `order_id` | если не задан — в платеже будет сгенерированный SDK | уходит **как есть**, SDK не подставляет и не переписывает | | Свой ключ | `idempotency_key` **не существует** | можно передать `idempotency_key` — уйдёт в заголовок | В обеих версиях таймаут/обрыв сети не создаст дубль счёта или перевода. Для **выплат** `order_id` обязателен всегда — его задаёте вы (иначе `payout.order_id_required`). *** ## Свой HTTP-транспорт (необязательно) По умолчанию — cURL. Хотите Guzzle / PSR-18 / мок для тестов — реализуйте интерфейс `Oblodai\Http\Transport`: ```php theme={null} $client = new Oblodai\Client($id, $secret, ['transport' => new MyTransport()]); ``` *** ## Логи и отладка Включите логи одной переменной окружения — увидите каждый запрос/ответ/ретрай: ```bash theme={null} export OBLODAI_LOG=debug # уровни: debug · info · warn · error ``` Строки идут в stderr с префиксом `oblodai:` (метод, путь, статус, номер попытки, задержка ретрая). **Секреты, подпись и тела запросов в лог не попадают.** Вместо переменной можно задать свой логгер — опция `logger` в конфиге: ```php theme={null} $client = new Oblodai\Client($publicId, $secret, [ 'logger' => fn (string $lvl, string $msg) => error_log("$lvl $msg"), ]); ``` **Проверяете подпись вебхука статичным (старым) вектором?** По умолчанию действует окно свежести 5 минут — старый `timestamp` отклонит replay-защита (валидная подпись → всё равно ошибка). Для офлайн-проверки задайте окно 0: `Webhooks::constructEvent($secret, $raw, $ts, $sig, maxAgeSeconds: 0)`. *** ## Если не получилось | Симптом | Причина и что делать | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `401` / `ApiException` на каждый вызов | Не заданы `OBLODAI_PUBLIC_ID`/`OBLODAI_SECRET` или перепутаны местами. SDK подписывает сам — руками ничего считать не надо. | | `ConfigException` при `fromEnv()` | Переменная окружения не задана (её имя — в тексте ошибки). | | Вебхук не проходит проверку | Передавайте **сырое** тело (`file_get_contents('php://input')`) и **webhook‑секрет** из `webhooks()->register()`, а не API‑секрет. | | Вебхук вообще не приходит | Не зарегистрировали endpoint (`webhooks()->register($url)`) или сайт недоступен из интернета. | | `429` | SDK повторяет сам с учётом `Retry-After`. Не поллите `payments()->info()` в цикле. Много операций — шлите их [пачкой](/guides/batch-operations). | Полная диагностика — [Что делать, если не работает](/guides/troubleshooting). ## Связанные страницы # Python SDK Source: https://docs.oblodai.com/sdk/python Официальная библиотека для Python. Пакет `oblodai`. Синхронный и асинхронный клиенты, типы (pydantic v2). **Требования:** Python 3.9+. *** ## Установка ```bash theme={null} pip install oblodai ``` **Новое в v1.1.0** (текущая версия в PyPI): группы `batches` / `links` / `splits` / `payout_links`, методы `create_batch` / `refund_batch` / `send_email` / `resolve` — и **ломающее изменение** идемпотентности: вместо авто-`order_id` теперь заголовок `Idempotency-Key` (см. «Ошибки и повторы»). *** ## Аутентификация (ключи из окружения) Ключи берутся в кабинете [my.oblodai.com](https://my.oblodai.com) — см. [Регистрация и ключи](/guides/get-keys). ```bash theme={null} export OBLODAI_PUBLIC_ID=oblodai_ваш_public_id export OBLODAI_SECRET=oblodai_ваш_secret # необязательно: export OBLODAI_BASE_URL=https://api.oblodai.com ``` ```python theme={null} from oblodai import OblodaiClient client = OblodaiClient.from_env() # OBLODAI_PUBLIC_ID / OBLODAI_SECRET / OBLODAI_BASE_URL ``` Или явно: ```python theme={null} client = OblodaiClient(public_id="...", secret="...", base_url="https://api.oblodai.com") ``` *** ## Быстрый старт: принять платёж ```python theme={null} payment = client.payments.create( amount="10", currency="USD", order_id="order-1", to_currency="USDT", network="tron", url_callback="https://ваш-сайт.ру/oblodai/webhook", url_success="https://ваш-сайт.ру/thanks", ) # редиректите покупателя на payment.url print(payment.address, payment.url) ``` Не хотите указывать валюту/сеть за покупателя? Не передавайте `to_currency` и `network` — получится «валюто-агностичная» ссылка, где покупатель сам выберет монету на странице оплаты. ### Асинхронно ```python theme={null} from oblodai import AsyncOblodaiClient async with AsyncOblodaiClient.from_env() as client: payment = await client.payments.create(amount="10", currency="USD", order_id="order-1") ``` *** ## Ресурсы и методы | Группа | Методы | | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client.payments` | `create`, `info`, `history`, `services`, `qr`, `resend`, `refund`, `list_accepted`, `set_accepted`, `get_accuracy`, `set_accuracy`, `get_autorefund`, `set_autorefund`, `set_discount`, `list_discounts` · **с v1.1.0:** `create_batch`, `refund_batch`, `send_email`, `resolve` | | `client.payouts` | `create`, `create_mass`, `info`, `history`, `services`, `calculate`, `approve`, `get_fee_config`, `set_fee_config`, `get_refund_fee_config`, `set_refund_fee_config`, `refund` · **с v1.1.0:** `create_batch` | | `client.batches` **(v1.1.0)** | `info` | | `client.links` **(v1.1.0)** | `create`, `list`, `info`, `toggle`, `public_get`, `checkout` | | `client.payout_links` **(v1.1.0)** | `create`, `create_batch` (до 500), `list`, `info`, `cancel` + публичные без подписи: `claim_info(token)`, `claim(token, address=…, memo=…)` | | `client.splits` **(v1.1.0)** | `split_to_address`, `split_to_merchant`, `list_rules`, `delete_rule`, `get_config`, `set_config` | | `client.wallets` | `create`, `block`, `blocked_address_refund`, `qr` | | `client.account` | `balance`, `referral`, `transfer_to_personal`, `vrcs` | | `client.webhooks` | `register`, `deliveries`, `test_payment`, `test_wallet`, `test_payout` | | `client.settings` | `list_auto_withdraw`, `set_auto_withdraw`, `delete_auto_withdraw`, `list_allowlist`, `add_allowlist`, `remove_allowlist`, `enable_allowlist` | | `client.rates` | `list` (курсы), `currencies` (публичный каталог) | Точные поля — в [Справочнике](/reference/overview). У синхронного и асинхронного клиента API одинаков. Всё, что помечено «с v1.1.0», доступно начиная с версии **1.1.0** — если у вас стоит v1.0.x, обновите пакет (`pip install -U oblodai`). Свой ключ идемпотентности в методах создания — kwarg `idempotency_key` (уйдёт в заголовок). ```python theme={null} # Массовые операции: до 5000 элементов одним подписанным запросом (одна отметка rate-limit). sub = client.payments.create_batch([ {"amount": "10", "currency": "USD", "order_id": "a-1", "to_currency": "USDT", "network": "tron"}, {"amount": "20", "currency": "EUR", "order_id": "a-2", "to_currency": "USDT", "network": "tron"}, ], on_error="continue") # "continue" (по умолчанию) или "stop" info = client.batches.info(sub.batch_id, limit=100) # прогресс и результат по каждому элементу if info.done: print(info.succeeded, info.failed) # Платёжная ссылка: платят многие, каждый платёж — свой инвойс. Работает без вашего бэкенда. link = client.links.create(amount_mode="open", currency="USD") # Сплит: доля каждого входящего платежа автоматически уходит партнёру. client.splits.split_to_address(address="T...", network="tron", percent=10, note="партнёр А") # Счёт на e-mail (письмо с кнопкой «Оплатить»). client.payments.send_email(uuid=payment.uuid, email="buyer@example.com") # Резолв недоплаты: принять частичную оплату (глушит авто-возврат) или вернуть плательщику. client.payments.resolve(uuid=payment.uuid, action="accept") # или action="refund" ``` **Крипто-чек за 4 строки** — выплата без адреса получателя (заберёт сам по ссылке): ```python theme={null} check = client.payout_links.create( amount="25", currency="USDT", network="tron", reference="bonus-42", expires_in_hours=72, email="winner@example.com", ) print(check.claim_url) # отдайте получателю — он введёт свой адрес сам ``` * Задавайте `expires_in_hours` **явно**: без него чек живёт всего **1 час**. * `claim_url` / `claim_token` возвращаются **только из `create`, один раз** — сохраните сразу. * Дедупликация — поле `reference` (заголовок `Idempotency-Key` на этих эндпоинтах не действует). Подробности — [Массовые операции](/guides/batch-operations), [Платёжные ссылки](/guides/payment-links), [Сплит-платежи](/guides/split-payments), [Счета на e-mail](/guides/email-invoices), [Крипто-чеки](/guides/payout-links), [Резолв платежа](/reference/payment-resolve). *** ## Валюта цены и валюта расчёта В примере выше `currency="USD"` — **валюта цены**, а `to_currency="USDT"` — **валюта расчёта**. Это разные вещи: * **`currency` (цена)** — одна из **23 фиатных валют**: USD, EUR, GBP, RUB, UAH, PLN, CZK, TRY, CNY, INR, BRL, CAD, AUD, CHF, AED, ZAR, MXN, IDR, THB, VND, NGN, JPY, KRW — **или любая монета** (USDT, BTC, TRX, …). * **`to_currency` (расчёт)** — **только крипта.** Фиата здесь не бывает: баланс, выплаты и возвраты всегда в монете, шлюз не хранит фиат. * У **JPY и KRW ноль знаков** после запятой (`amount="10000"`, не `"10000.00"`); у остальных — 2. * **KZT, KGS, UZS пока не поддерживаются** → `payment.unknown_currency`. * Фиатная цена + **только** `network` без `to_currency` → `400 payment.to_currency_required`. Либо задайте `to_currency`, либо не задавайте **ни то, ни другое** (тогда монету выберет покупатель). Полный список валют цены — `client.rates.currencies()`, поле `pricing_currencies`. *** ## Проверьте, что заработало 1. Запустите код создания платежа из «Быстрого старта». В ответе должен прийти `url` — это ссылка на hosted-страницу оплаты. Откройте её в браузере: если страница открылась, приём платежей настроен. 2. Чтобы реально **поймать вебхук** об оплате на локальной машине, нужен публичный HTTPS-адрес — поднимите туннель (ngrok / cloudflared) и укажите его URL в `url_callback`. Подробнее — в [Тестировании](/guides/testing) и [Настройке вебхуков](/guides/webhooks-setup). *** ## Вебхуки ```python Шаг 1. Регистрация (один раз) theme={null} endpoint = client.webhooks.register("https://ваш-сайт.ру/oblodai/webhook") webhook_secret = endpoint.secret # сохраните — им проверяются вебхуки (не API-секрет!) ``` ```python Шаг 2. Приём (Flask, сырое тело) theme={null} from flask import Flask, request from oblodai import verify_webhook, OblodaiSignatureError app = Flask(__name__) @app.post("/oblodai/webhook") def webhook(): raw = request.get_data() # сырые байты, не request.json! if b'"is_test"' in raw: # тестовые вебхуки не подписаны return "", 200 try: # третий аргумент — сами заголовки (mapping); функция сама достанет # X-Webhook-Timestamp / X-Webhook-Signature и проверит окно свежести. verify_webhook(webhook_secret, raw, request.headers) except OblodaiSignatureError: return "", 403 event = request.get_json() info = client.payments.info(uuid=event["uuid"]) if info.payment_status in ("paid", "paid_over"): ... # пометить заказ info.order_id оплаченным (идемпотентно) return "", 200 ``` Оплаченными считайте только `paid` и `paid_over`. *** ## Ошибки и повторы ```python theme={null} from oblodai import OblodaiAPIError try: client.payouts.create(...) except OblodaiAPIError as e: e.code # "payout.insufficient_funds" — ветвитесь по коду e.status # HTTP-статус e.is_retriable ``` Клиент **сам** повторяет 5xx/429/сетевые сбои с backoff и учётом `Retry-After` — **повторы включены по умолчанию**. Отключить: `retry=None`. **Повтор безопасен, но механизм зависит от версии:** | | **v1.0.x** (предыдущая) | **v1.1.0** (текущая, в PyPI) | | ---------------- | --------------------------------------------------- | ------------------------------------------------------------------- | | Защита от дублей | `order_id`: не задали — **SDK подставит свой** | заголовок `Idempotency-Key`, одинаковый во всех внутренних повторах | | Ваш `order_id` | если не задан — в платеже будет сгенерированный SDK | уходит **как есть**, SDK не подставляет и не переписывает | | Свой ключ | `idempotency_key` **не существует** | можно передать `idempotency_key` — уйдёт в заголовок | В обеих версиях таймаут/обрыв сети не создаст дубль счёта или перевода. Для **выплат** `order_id` обязателен всегда — его задаёте вы (иначе `payout.order_id_required`). *** ## Логи и отладка Включите логи одной переменной окружения — увидите каждый запрос/ответ/ретрай: ```bash theme={null} export OBLODAI_LOG=debug # уровни: debug · info · warning · error ``` Строки идут в stderr с префиксом `oblodai:` (метод, путь, статус, номер попытки, задержка ретрая). **Секреты, подпись и тела запросов в лог не попадают.** Вместо переменной можно задать свой логгер: настройте логгер `logging.getLogger("oblodai")`. **Проверяете подпись вебхука статичным (старым) вектором?** По умолчанию действует окно свежести 5 минут — старый `timestamp` отклонит replay-защита (валидная подпись → всё равно ошибка). Для офлайн-проверки задайте окно 0: `verify_webhook(secret, raw, headers, max_age_seconds=0)`. *** ## Если не получилось | Симптом | Причина и что делать | | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `OblodaiAPIError` `401` на каждый вызов | Не заданы `OBLODAI_PUBLIC_ID`/`OBLODAI_SECRET` или перепутаны. SDK подписывает сам. | | `ValueError` при `from_env()` | Обязательная переменная окружения не задана (её имя — в тексте ошибки). | | Вебхук не проходит проверку | Нужно **сырое** тело — `request.get_data()`, а **не** `request.json`; и **webhook‑секрет** из `webhooks.register()`, не API‑секрет. | | Вебхук вообще не приходит | Не зарегистрировали endpoint (`webhooks.register(url)`) или сайт недоступен из интернета. | | `429` | SDK повторяет сам с учётом `Retry-After`. Не поллите `payments.info()` в цикле. Много операций — шлите их [пачкой](/guides/batch-operations). | Полная диагностика — [Что делать, если не работает](/guides/troubleshooting). ## Связанные страницы # Rust SDK Source: https://docs.oblodai.com/sdk/rust Официальная библиотека для Rust. Крейт `oblodai`. Синхронный клиент на `reqwest` (blocking) с инъектируемым транспортом. **Требования:** Rust **1.75+**, edition 2021. *** ## Установка ```bash theme={null} cargo add oblodai cargo add serde_json # нужен для serde_json::json!({...}) в примерах ``` **Новое в v1.1.0** (текущая версия в crates.io): группы `batches()` / `payment_links()` / `splits()` / `payout_links()`, методы `*_batch` / `send_email` / `resolve` — и **ломающее изменение** идемпотентности: вместо авто-`order_id` теперь заголовок `Idempotency-Key` (см. «Ошибки и повторы»). По умолчанию включена фича `reqwest-client` (встроенный HTTP-клиент). Для своего транспорта отключите дефолтные фичи и реализуйте трейт `HttpTransport`. *** ## Аутентификация (ключи из окружения) Ключи берутся в кабинете [my.oblodai.com](https://my.oblodai.com) — см. [Регистрация и ключи](/guides/get-keys). ```bash theme={null} export OBLODAI_PUBLIC_ID=oblodai_ваш_public_id export OBLODAI_SECRET=oblodai_ваш_secret # необязательно: export OBLODAI_BASE_URL=https://api.oblodai.com ``` ```rust theme={null} // читает OBLODAI_PUBLIC_ID / OBLODAI_SECRET / OBLODAI_BASE_URL let client = oblodai::Client::from_env()?; ``` Или явно: ```rust theme={null} use oblodai::{Client, Config}; let client = Client::new( Config::new("public_id", "secret").base_url("https://api.oblodai.com"), )?; ``` *** ## Быстрый старт: принять платёж ```rust theme={null} use serde_json::json; let payment = client.payments().create(json!({ "amount": "10", "currency": "USD", "order_id": "order-1", "to_currency": "USDT", "network": "tron", "url_callback": "https://ваш-сайт.ру/oblodai/webhook", }))?; println!("{}", payment.url); // редиректите покупателя сюда ``` Тела запроса передаются как `serde_json::json!({...})`; ответ — типизированная структура (`Payment`). Не хотите указывать валюту/сеть за покупателя? Не передавайте `to_currency` и `network` — получится «валюто-агностичная» ссылка, где покупатель сам выберет монету на странице оплаты. *** ## Ресурсы и методы Доступ через `client.название()`: | Группа | Методы | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client.payments()` | `create`, `info`, `history`, `services`, `qr`, `resend`, `refund`, `list_accepted`, `set_accepted`, `get_accuracy`, `set_accuracy`, `get_autorefund`, `set_autorefund`, `set_discount`, `list_discounts` · **с v1.1.0:** `create_batch`, `refund_batch`, `send_email`, `resolve` | | `client.payouts()` | `create`, `create_mass`, `info`, `history`, `services`, `calculate`, `approve`, `get_fee_config`, `set_fee_config`, `get_refund_fee_config`, `set_refund_fee_config`, `refund` · **с v1.1.0:** `create_batch` | | `client.batches()` **(v1.1.0)** | `info` | | `client.payment_links()` **(v1.1.0)** | `create`, `list`, `info`, `toggle`, `public_get`, `checkout` | | `client.payout_links()` **(v1.1.0)** | `create`, `create_batch` (до 500), `list`, `info`, `cancel` + публичные без подписи: `claim_info(token)`, `claim(token, address, memo)` | | `client.splits()` **(v1.1.0)** | `create_rule`, `split_to_address`, `split_to_merchant`, `list_rules`, `delete_rule`, `get_config`, `set_config` | | `client.wallets()` | `create`, `block`, `blocked_address_refund`, `qr` | | `client.account()` | `balance`, `referral`, `transfer_to_personal`, `vrcs` | | `client.webhooks()` | `register`, `deliveries`, `test_payment`, `test_wallet`, `test_payout` | | `client.settings()` | `list_auto_withdraw`, `set_auto_withdraw`, `delete_auto_withdraw`, `list_allowlist`, `add_allowlist`, `remove_allowlist`, `enable_allowlist` | | `client.rates()` | `list` (курсы), `currencies` (публичный каталог) | Точные поля — в [Справочнике](/reference/overview). Всё, что помечено «с v1.1.0», доступно начиная с версии **1.1.0** — если у вас стоит v1.0.x, обновите крейт (`cargo update -p oblodai`). Свой ключ идемпотентности в методах создания — поле `idempotency_key` в `json!`-параметрах (уйдёт в заголовок). Тела запросов везде передаются как `serde_json::json!({...})` — отдельных билдеров параметров в крейте нет. ```rust theme={null} use oblodai::ResolveAction; use serde_json::json; // Массовые операции: до 5000 элементов одним подписанным запросом (одна отметка rate-limit). let sub = client.payments().create_batch(vec![ json!({"amount": "10", "currency": "USD", "order_id": "a-1", "to_currency": "USDT", "network": "tron"}), json!({"amount": "20", "currency": "EUR", "order_id": "a-2", "to_currency": "USDT", "network": "tron"}), ], Some("continue"))?; // "continue" (по умолчанию) или "stop" let info = client.batches().info(&sub.batch_id, Some(100), Some(0))?; // Платёжная ссылка: платят многие, каждый платёж — свой инвойс. Работает без вашего бэкенда. let link = client.payment_links().create(json!({"amount_mode": "open", "currency": "USD", "title": "Донат"}))?; // Сплит: доля каждого входящего платежа автоматически уходит партнёру. // Есть и общий create_rule(json!({...})), и обёртки: let rule = client.splits().split_to_address("T...", "tron", 10.0, Some("партнёр А"))?; // Счёт на e-mail (письмо с кнопкой «Оплатить»). client.payments().send_email(Some(&payment.uuid), None, Some("buyer@example.com"))?; // Резолв недоплаты: принять частичную оплату (глушит авто-возврат) или вернуть плательщику. client.payments().resolve(ResolveAction::Accept, json!({"uuid": payment.uuid}))?; // или ResolveAction::Refund ``` **Крипто-чек за 4 строки** — выплата без адреса получателя (заберёт сам по ссылке): ```rust theme={null} let check = client.payout_links().create(json!({ "amount": "25", "currency": "USDT", "network": "tron", "reference": "bonus-42", "expires_in_hours": 72, "email": "winner@example.com", }))?; println!("{}", check.claim_url); // отдайте получателю — он введёт свой адрес сам ``` * Задавайте `expires_in_hours` **явно**: без него чек живёт всего **1 час**. * `claim_url` / `claim_token` возвращаются **только из `create`, один раз** — сохраните сразу. * Дедупликация — поле `reference` (заголовок `Idempotency-Key` на этих эндпоинтах не действует). Подробности — [Массовые операции](/guides/batch-operations), [Платёжные ссылки](/guides/payment-links), [Сплит-платежи](/guides/split-payments), [Счета на e-mail](/guides/email-invoices), [Крипто-чеки](/guides/payout-links), [Резолв платежа](/reference/payment-resolve). *** ## Валюта цены и валюта расчёта В примере выше `"currency": "USD"` — **валюта цены**, а `"to_currency": "USDT"` — **валюта расчёта**. Это разные вещи: * **`currency` (цена)** — одна из **23 фиатных валют**: USD, EUR, GBP, RUB, UAH, PLN, CZK, TRY, CNY, INR, BRL, CAD, AUD, CHF, AED, ZAR, MXN, IDR, THB, VND, NGN, JPY, KRW — **или любая монета** (USDT, BTC, TRX, …). * **`to_currency` (расчёт)** — **только крипта.** Фиата здесь не бывает: баланс, выплаты и возвраты всегда в монете, шлюз не хранит фиат. * У **JPY и KRW ноль знаков** после запятой (`"amount": "10000"`, не `"10000.00"`); у остальных — 2. * **KZT, KGS, UZS пока не поддерживаются** → `payment.unknown_currency`. * Фиатная цена + **только** `network` без `to_currency` → `400 payment.to_currency_required`. Либо задайте `to_currency`, либо не задавайте **ни то, ни другое** (тогда монету выберет покупатель). Полный список валют цены — `client.rates().currencies()`, поле `pricing_currencies`. *** ## Проверьте, что заработало 1. Запустите код создания платежа из «Быстрого старта». В ответе должен прийти `url` — это ссылка на hosted-страницу оплаты. Откройте её в браузере: если страница открылась, приём платежей настроен. 2. Чтобы реально **поймать вебхук** об оплате на локальной машине, нужен публичный HTTPS-адрес — поднимите туннель (ngrok / cloudflared) и укажите его URL в `url_callback`. Подробнее — в [Тестировании](/guides/testing) и [Настройке вебхуков](/guides/webhooks-setup). *** ## Вебхуки ```rust Шаг 1. Регистрация (один раз) theme={null} let ep = client.webhooks().register("https://ваш-сайт.ру/oblodai/webhook")?; // сохраните ep.secret — им проверяются вебхуки (не API-секрет!) ``` ```rust Шаг 2. Приём (обязательно СЫРОЕ тело) theme={null} use oblodai::{verify_webhook, WebhookHeaders, VerifyOptions}; let headers = WebhookHeaders { timestamp: req_header("X-Webhook-Timestamp"), signature: req_header("X-Webhook-Signature"), }; // распарсьте тело: при is_test == true ответьте 200 и выходите — пробные тела не подписаны match verify_webhook(&webhook_secret, raw_body_bytes, &headers, &VerifyOptions::default()) { Ok(_) => { /* подпись верна */ } Err(_) => { /* 403 */ } } let event: serde_json::Value = serde_json::from_slice(raw_body_bytes)?; let uuid = event["uuid"].as_str().unwrap_or_default(); let info = client.payments().info(Some(&uuid), None)?; if info.payment_status == "paid" || info.payment_status == "paid_over" { // пометить заказ info.order_id оплаченным (идемпотентно) } ``` Тестовые вебхуки (`is_test`) не подписаны — распарсьте тело и при `is_test == true` отвечайте 200 **до** вызова `verify_webhook`. *** ## Ошибки и повторы ```rust theme={null} match client.payouts().create(json!({ /* ... */ })) { Ok(p) => { /* ... */ } Err(oblodai::Error::Api { code, status, .. }) => { // ветвитесь по code, напр. "payout.insufficient_funds" } Err(e) => { /* сеть/сериализация */ } } ``` Клиент **сам** повторяет 5xx/429/сетевые сбои с backoff и учётом `Retry-After`. **Повторы включены по умолчанию** — `Config::new(...)` и `Config::from_env()` уже возвращают конфиг с `RetryConfig::default()`, специально ничего задавать не надо. Отключить: `Config::new(id, secret).retry(None)`. **Повтор безопасен, но механизм зависит от версии:** | | **v1.0.x** (предыдущая) | **v1.1.0** (текущая, в crates.io) | | ---------------- | --------------------------------------------------- | ------------------------------------------------------------------- | | Защита от дублей | `order_id`: не задали — **SDK подставит свой** | заголовок `Idempotency-Key`, одинаковый во всех внутренних повторах | | Ваш `order_id` | если не задан — в платеже будет сгенерированный SDK | уходит **как есть**, SDK не подставляет и не переписывает | | Свой ключ | `idempotency_key` **не существует** | можно передать `idempotency_key` — уйдёт в заголовок | В обеих версиях таймаут/обрыв сети не создаст дубль счёта или перевода. Для **выплат** `order_id` обязателен всегда — его задаёте вы (иначе `payout.order_id_required`). *** ## Логи и отладка Включите логи одной переменной окружения — увидите каждый запрос/ответ/ретрай: ```bash theme={null} export OBLODAI_LOG=debug # уровни: debug · info · warn · error ``` Строки идут в stderr с префиксом `oblodai:` (метод, путь, статус, номер попытки, задержка ретрая). **Секреты, подпись и тела запросов в лог не попадают.** Вместо переменной можно задать свой логгер: `Config::new(..).logger(Arc::new(|level, msg| ...))`. **Проверяете подпись вебхука статичным (старым) вектором?** По умолчанию действует окно свежести 5 минут — старый `timestamp` отклонит replay-защита (валидная подпись → всё равно ошибка). Для офлайн-проверки задайте окно 0: `verify_webhook(secret, raw, &headers, &VerifyOptions{ max_age_seconds: 0, ..Default::default() })`. *** ## Если не получилось | Симптом | Причина и что делать | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Error::Api` `401` на каждый вызов | Не заданы `OBLODAI_PUBLIC_ID`/`OBLODAI_SECRET` или перепутаны. SDK подписывает сам. | | `from_env()` возвращает `Err` | Обязательная переменная окружения не задана (её имя — в тексте ошибки). | | Вебхук не проходит проверку | Проверяйте по **сырым** байтам тела и **webhook‑секрету** из `webhooks().register()`, не API‑секрету. | | Вебхук вообще не приходит | Не зарегистрировали endpoint (`webhooks().register(url)`) или сайт недоступен из интернета. | | `429` | SDK повторяет сам с учётом `Retry-After` (повторы включены по умолчанию). Не поллите `payments().info()` в цикле. Много операций — шлите их [пачкой](/guides/batch-operations). | Полная диагностика — [Что делать, если не работает](/guides/troubleshooting). ## Связанные страницы # TypeScript / Node.js SDK Source: https://docs.oblodai.com/sdk/typescript Официальная библиотека для Node.js и TypeScript. Пакет `@oblodai-npm/sdk`. Типы «из коробки», работает и в JS, и в TS. **Требования:** Node.js 18+. *** ## Установка ```bash theme={null} npm install @oblodai-npm/sdk # или: pnpm add @oblodai-npm/sdk / yarn add @oblodai-npm/sdk ``` Пакет выходит в npm под scope **`@oblodai-npm`** — ставьте именно `@oblodai-npm/sdk`. **Новое в v1.1.0** (текущая версия в npm): группы `batches` / `links` / `splits` / `payoutLinks`, методы `createBatch` / `refundBatch` / `sendEmail` / `resolve` — и **ломающее изменение** идемпотентности: вместо авто-`order_id` теперь заголовок `Idempotency-Key` (см. «Ошибки и повторы»). *** ## Аутентификация (ключи из окружения) Ключи берутся в кабинете [my.oblodai.com](https://my.oblodai.com) — см. [Регистрация и ключи](/guides/get-keys). ```bash theme={null} export OBLODAI_PUBLIC_ID=oblodai_ваш_public_id export OBLODAI_SECRET=oblodai_ваш_secret # необязательно: export OBLODAI_BASE_URL=https://api.oblodai.com ``` ```ts theme={null} import { OblodaiClient } from '@oblodai-npm/sdk'; const client = OblodaiClient.fromEnv(); // OBLODAI_PUBLIC_ID / OBLODAI_SECRET / OBLODAI_BASE_URL ``` Или явно: ```ts theme={null} const client = new OblodaiClient({ publicId: process.env.OBLODAI_PUBLIC_ID!, secret: process.env.OBLODAI_SECRET!, baseUrl: 'https://api.oblodai.com', // необязательно }); ``` *** ## Быстрый старт: принять платёж ```ts theme={null} const payment = await client.payments.create({ amount: '10', currency: 'USD', order_id: 'order-1', to_currency: 'USDT', network: 'tron', url_callback: 'https://ваш-сайт.ру/oblodai/webhook', url_success: 'https://ваш-сайт.ру/thanks', }); res.redirect(payment.url); // отправляем покупателя оплачивать ``` Не хотите указывать валюту/сеть за покупателя? Не передавайте `to_currency` и `network` — получится «валюто-агностичная» ссылка, где покупатель сам выберет монету на странице оплаты. *** ## Ресурсы и методы | Группа | Методы | | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client.payments` | `create`, `info`, `history`, `services`, `qr`, `resend`, `refund`, `listAccepted`, `setAccepted`, `getAccuracy`, `setAccuracy`, `getAutorefund`, `setAutorefund`, `setDiscount`, `listDiscounts` · **с v1.1.0:** `createBatch`, `refundBatch`, `sendEmail`, `resolve` | | `client.payouts` | `create`, `createMass`, `info`, `history`, `services`, `calculate`, `approve`, `getFeeConfig`, `setFeeConfig`, `getRefundFeeConfig`, `setRefundFeeConfig`, `refund` · **с v1.1.0:** `createBatch` | | `client.batches` **(v1.1.0)** | `info` | | `client.links` **(v1.1.0)** | `create`, `list`, `info`, `toggle`, `publicGet`, `checkout` · `client.links` = синоним `client.paymentLinks` | | `client.payoutLinks` **(v1.1.0)** | `create`, `createBatch` (до 500), `list`, `info`, `cancel` + публичные без подписи: `claimInfo(token)`, `claim(token, {address, memo?})` | | `client.splits` **(v1.1.0)** | `splitToAddress`, `splitToMerchant`, `listRules`, `deleteRule`, `getConfig`, `setConfig` | | `client.wallets` | `create`, `block`, `blockedAddressRefund`, `qr` | | `client.account` | `balance`, `referral`, `transferToPersonal`, `vrcs` | | `client.webhooks` | `register`, `deliveries`, `testPayment`, `testWallet`, `testPayout` | | `client.settings` | `listAutoWithdraw`, `setAutoWithdraw`, `deleteAutoWithdraw`, `listAllowlist`, `addAllowlist`, `removeAllowlist`, `enableAllowlist` | | `client.rates` | `list` (курсы), `currencies` (публичный каталог) | Все методы возвращают `Promise` с уже развёрнутым `result`. Точные поля — в [Справочнике](/reference/overview). Всё, что помечено «с v1.1.0», доступно начиная с версии **1.1.0** — если у вас стоит v1.0.x, обновите пакет (`npm update @oblodai-npm/sdk`). Свой ключ идемпотентности в методах создания — параметр `idempotency_key` (уйдёт в заголовок). ```ts theme={null} // Массовые операции: до 5000 элементов одним подписанным запросом (одна отметка rate-limit). const sub = await client.payments.createBatch( [ { amount: '10', currency: 'USD', order_id: 'a-1', to_currency: 'USDT', network: 'tron' }, { amount: '20', currency: 'EUR', order_id: 'a-2', to_currency: 'USDT', network: 'tron' }, ], { onError: 'continue' }, // 'continue' (по умолчанию) или 'stop' ); const info = await client.batches.info(sub.batch_id, { limit: 100 }); // прогресс и результат по элементам // Платёжная ссылка: платят многие, каждый платёж — свой инвойс. Работает без вашего бэкенда. const link = await client.links.create({ amount_mode: 'open', currency: 'USD', title: 'Донат' }); // Сплит: доля каждого входящего платежа автоматически уходит партнёру. await client.splits.splitToAddress('T...', 'tron', 10, 'партнёр А'); // Счёт на e-mail (письмо с кнопкой «Оплатить»). await client.payments.sendEmail({ uuid: payment.uuid, email: 'buyer@example.com' }); // Резолв недоплаты: принять частичную оплату (глушит авто-возврат) или вернуть плательщику. await client.payments.resolve({ uuid: payment.uuid, action: 'accept' }); // или action: 'refund' ``` **Крипто-чек за 4 строки** — выплата без адреса получателя (заберёт сам по ссылке): ```ts theme={null} const check = await client.payoutLinks.create({ amount: '25', currency: 'USDT', network: 'tron', reference: 'bonus-42', expires_in_hours: 72, email: 'winner@example.com', }); console.log(check.claim_url); // отдайте получателю — он введёт свой адрес сам ``` * Задавайте `expires_in_hours` **явно**: без него чек живёт всего **1 час**. * `claim_url` / `claim_token` возвращаются **только из `create`, один раз** — сохраните сразу. * Дедупликация — поле `reference` (заголовок `Idempotency-Key` на этих эндпоинтах не действует). Подробности — [Массовые операции](/guides/batch-operations), [Платёжные ссылки](/guides/payment-links), [Сплит-платежи](/guides/split-payments), [Счета на e-mail](/guides/email-invoices), [Крипто-чеки](/guides/payout-links), [Резолв платежа](/reference/payment-resolve). *** ## Валюта цены и валюта расчёта В примере выше `currency: 'USD'` — **валюта цены**, а `to_currency: 'USDT'` — **валюта расчёта**. Это разные вещи: * **`currency` (цена)** — одна из **23 фиатных валют**: USD, EUR, GBP, RUB, UAH, PLN, CZK, TRY, CNY, INR, BRL, CAD, AUD, CHF, AED, ZAR, MXN, IDR, THB, VND, NGN, JPY, KRW — **или любая монета** (USDT, BTC, TRX, …). * **`to_currency` (расчёт)** — **только крипта.** Фиата здесь не бывает: баланс, выплаты и возвраты всегда в монете, шлюз не хранит фиат. * У **JPY и KRW ноль знаков** после запятой (`amount: '10000'`, не `'10000.00'`); у остальных — 2. * **KZT, KGS, UZS пока не поддерживаются** → `payment.unknown_currency`. * Фиатная цена + **только** `network` без `to_currency` → `400 payment.to_currency_required`. Либо задайте `to_currency`, либо не задавайте **ни то, ни другое** (тогда монету выберет покупатель). Полный список валют цены — `client.rates.currencies()`, поле `pricing_currencies`. *** ## Проверьте, что заработало 1. Запустите код создания платежа из «Быстрого старта». В ответе должен прийти `url` — это ссылка на hosted-страницу оплаты. Откройте её в браузере: если страница открылась, приём платежей настроен. 2. Чтобы реально **поймать вебхук** об оплате на локальной машине, нужен публичный HTTPS-адрес — поднимите туннель (ngrok / cloudflared) и укажите его URL в `url_callback`. Подробнее — в [Тестировании](/guides/testing) и [Настройке вебхуков](/guides/webhooks-setup). *** ## Вебхуки ```ts Шаг 1. Регистрация (один раз) и сохранение секрета theme={null} const endpoint = await client.webhooks.register('https://ваш-сайт.ру/oblodai/webhook'); // сохраните endpoint.secret — им проверяются все вебхуки (это НЕ ваш API-секрет) ``` ```ts Шаг 2. Приём (Express, сырое тело) theme={null} import express from 'express'; import { constructWebhookEvent, OblodaiSignatureError, type WebhookEvent } from '@oblodai-npm/sdk'; app.post('/oblodai/webhook', express.raw({ type: '*/*' }), async (req, res) => { const raw = req.body as Buffer; const maybe = JSON.parse(raw.toString('utf8')); if (maybe.is_test) return res.send('ok'); // тестовые вебхуки не подписаны let event: WebhookEvent; try { event = constructWebhookEvent(WEBHOOK_SECRET, raw, { timestamp: req.get('X-Webhook-Timestamp')!, signature: req.get('X-Webhook-Signature')!, }); } catch (e) { if (e instanceof OblodaiSignatureError) return res.status(403).send('bad signature'); throw e; } const info = await client.payments.info({ uuid: event.uuid }); if (info.payment_status === 'paid' || info.payment_status === 'paid_over') { // пометить заказ info.order_id оплаченным (идемпотентно) } res.send('ok'); }); ``` Оплаченными считайте только `paid` и `paid_over`. *** ## Ошибки и повторы ```ts theme={null} import { OblodaiApiError, OblodaiConnectionError } from '@oblodai-npm/sdk'; try { await client.payouts.create({ /* ... */ }); } catch (e) { if (e instanceof OblodaiApiError) { e.code; // "payout.insufficient_funds" — ветвитесь по коду e.status; // HTTP-статус e.isRetriable; // временная ли } } ``` Клиент **сам** повторяет 5xx/429/сетевые сбои с backoff и учётом `Retry-After` — **повторы включены по умолчанию**. Отключить: `new OblodaiClient({ ..., retry: false })`. **Повтор безопасен, но механизм зависит от версии:** | | **v1.0.x** (предыдущая) | **v1.1.0** (текущая, в npm) | | ---------------- | --------------------------------------------------- | ------------------------------------------------------------------- | | Защита от дублей | `order_id`: не задали — **SDK подставит свой** | заголовок `Idempotency-Key`, одинаковый во всех внутренних повторах | | Ваш `order_id` | если не задан — в платеже будет сгенерированный SDK | уходит **как есть**, SDK не подставляет и не переписывает | | Свой ключ | `idempotency_key` **не существует** | можно передать `idempotency_key` — уйдёт в заголовок | В обеих версиях таймаут/обрыв сети не создаст дубль счёта или перевода. Для **выплат** `order_id` обязателен всегда — его задаёте вы (иначе `payout.order_id_required`). *** ## Логи и отладка Включите логи одной переменной окружения — увидите каждый запрос/ответ/ретрай: ```bash theme={null} export OBLODAI_LOG=debug # уровни: debug · info · warn · error ``` Строки идут в stderr с префиксом `oblodai:` (метод, путь, статус, номер попытки, задержка ретрая). **Секреты, подпись и тела запросов в лог не попадают.** Вместо переменной можно задать свой логгер: `new OblodaiClient({ ..., logger })`. **Проверяете подпись вебхука статичным (старым) вектором?** По умолчанию действует окно свежести 5 минут — старый `timestamp` отклонит replay-защита (валидная подпись → всё равно ошибка). Для офлайн-проверки задайте окно 0: `constructWebhookEvent(secret, raw, hdrs, { maxAgeSeconds: 0 })`. *** ## Если не получилось | Симптом | Причина и что делать | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `OblodaiApiError` `401` на каждый вызов | Не заданы `OBLODAI_PUBLIC_ID`/`OBLODAI_SECRET` или перепутаны. SDK подписывает сам. | | `fromEnv()` бросает ошибку | Обязательная переменная окружения не задана (её имя — в тексте ошибки). | | Вебхук не проходит проверку | Нужно **сырое** тело — используйте `express.raw({ type: '*/*' })`, а не `express.json()`; и **webhook‑секрет** из `webhooks.register()`, не API‑секрет. | | Вебхук вообще не приходит | Не зарегистрировали endpoint (`webhooks.register(url)`) или сайт недоступен из интернета. | | `429` | SDK повторяет сам с учётом `Retry-After`. Не поллите `payments.info()` в цикле. Много операций — шлите их [пачкой](/guides/batch-operations). | Полная диагностика — [Что делать, если не работает](/guides/troubleshooting). ## Связанные страницы