# 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).
## Связанные страницы