RU

Merchant API — документация

Базовый URL: https://snapex.pro/api/v1/merchant

Ключ и первый запрос

  1. Получите ключ в кабинете

    Кабинет партнёра → Merchant → «Получить API-ключ». Ключ имеет вид smk_abcdefghij_ещё48символов и отображается один раз. При утрате выпустите новый: предыдущий ключ сразу станет недействительным.

  2. Добавьте один заголовок

    Authorization: Bearer ваш_ключ.

  3. Проверьте доступ

    Запросите баланс. Ответ с полем data означает, что ключ принят.

запрос
curl -sS https://snapex.pro/api/v1/merchant/balances \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"
ответ
{
  "data": [
    {
      "asset_id": "usdt",               // всегда usdt — другого баланса нет
      "available_atomic": "15000000",   // 15 USDT в минимальных единицах (6 знаков)
      "available": "15",                // то же значение в десятичном виде
      "held_atomic": "0",               // заморожено под незавершённые выплаты
      "held": "0"
    }
  ]
}

Если запрос не выполнен

Что пришло Почему
401 unauthorized Ключ не передан, содержит пробел или обрезан. После Bearer должен быть полный идентификатор smk_…
401 key_inactive Ключ отозван: выпущен новый, а в запросе указан предыдущий.

Пять правил

Большинство ошибок интеграции связано с нарушением одного из этих пунктов.

1. Суммы передавайте строкой

Передавайте либо amount — десятичное значение строкой: "0.001", "15", либо amount_atomic — целое в минимальных единицах: "100000" для 0.001 BTC. Укажите ровно одно поле. В ответах возвращаются обе формы: рядом с amount_atomic есть amount.

пример
"amount": "0.001"        // так можно
"amount_atomic": "100000" // и так можно
// оба сразу — ошибка 422

2. asset_id берите только из /assets

Допустимые коды: usdt, btc, xmr. Произвольные значения и идентификаторы из блокчейн-эксплорера отклоняются с кодом 422.

пример
"asset_id": "btc"   // правильно
"asset_id": "BTC"   // ошибка
"asset_id": "bitcoin" // ошибка

3. Idempotency-Key — на каждый создающий запрос

Уникальный идентификатор попытки. Если соединение прервалось и результат неизвестен, повторите запрос с тем же ключом: вернётся исходный результат, повторная операция не создастся. Обязателен для POST /invoices, POST /payments, POST /payouts/preview, POST /payouts.

пример
Idempotency-Key: order-42-payment   // первая попытка → 201, создано
Idempotency-Key: order-42-payment   // повтор → 200, то же самое
Idempotency-Key: order-42-payment   // но с другой суммой → 409 conflict

4. Пустых полей в ответе нет

Поля со значением null и пустые строки в ответ не включаются. У неоплаченного платежа отсутствуют actual_amount, credited_amount и fee. Проверяйте наличие ключа, а не его значение.

пример
// неоплаченный платёж:
{ "payment_id": "pay_…", "status": "awaiting_deposit" }
// поля credited_amount здесь просто нет

5. Не закрывайте заказ по статусу expired

Статус expired не означает, что перевод уже не поступит. Адрес отслеживается ещё около 6 часов, опоздавший перевод будет зачислен. Финальные статусы с зачислением: confirmed, confirmed_underpaid, confirmed_overpaid.

Сценарии с примерами

Каждый сценарий описывает полный цикл: последовательность запросов, формат ответа и назначение полей. Комментарии в примерах JSON добавлены для пояснения и в реальных ответах отсутствуют.

Приём оплаты

Клиент платит BTC за заказ на 60 USD

Заказ order-42. Требуется получить около 60 USDT; при курсе BTC около 60 000 ожидаемая сумма от клиента — 0.001 BTC.

1
Получите список доступных активов

Запрос возвращает код актива и decimals — число знаков после запятой.

запрос
curl -sS https://snapex.pro/api/v1/merchant/assets \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

GET /assets
Параметры не требуются.
Authorization
Единственный обязательный заголовок API.
ответ
{
  "data": [
    {
      "asset_id": "btc",       // ← значение для последующих запросов
      "symbol": "BTC",         // для отображения пользователю
      "network": "bitcoin",    // используется в выплатах
      "decimals": 8,           // 1 BTC = 100000000 атомарных единиц
      "price_usd": "60000"     // справочное значение, не для расчётов
    },
    {
      "asset_id": "usdt",
      "symbol": "USDT",
      "network": "TRON",
      "decimals": 6,
      "price_usd": "1.00"
    }
  ]
}
2
Фиксируем курс на 10 минут

Фиксирует курс на время, пока клиент находится на странице оплаты. Платёж и адрес на этом шаге не создаются: выполняется только расчёт. В ответе — сумма USDT к зачислению за 0.001 BTC.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments/credit-preview \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"asset_id":"btc","amount":"0.001"}'

Что здесь важно

asset_id: "btc"
Код из предыдущего шага.
amount: "0.001"
Ожидаемая сумма BTC от клиента. Передаётся строкой, не числом.
Idempotency-Key
Необязателен: операция не создаётся. Можно указать, чтобы повторно получить тот же расчёт.
ответ
{
  "data": {
    "credit_lock_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                                       // ← сохраните для следующего шага
    "asset_id": "btc",
    "expected_amount": "0.001",        // ожидаемая сумма от клиента
    "expected_amount_atomic": "100000",
    "credit_usdt": "59.4",             // ← столько будет зачислено на баланс
    "credit_usdt_atomic": "59400000",
    "expires_at": "2026-08-11T17:20:00+00:00"
                                       // ← после этого времени credit-preview недействителен
  }
}

Клиенту покажите expected_amount — сколько BTC отправить.

3
Создаём платёж и получаем адрес

На этом шаге создаётся платёж и выдаётся адрес для перевода. Сумму в запросе указывать не нужно — она уже задана в credit-preview на шаге 2.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: order-42-payment" \
  -d '{
    "credit_lock_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "external_id": "order-42"
  }'

Что здесь важно

credit_lock_id
Идентификатор из ответа credit-preview. Если срок фиксации истёк, вернётся 422 — нужно запросить новый preview.
external_id: "order-42"
Номер заказа на вашей стороне. Используется для поиска платежа и исключения дублей на один заказ.
Idempotency-Key
Обязателен. Используйте стабильный уникальный идентификатор, например номер заказа с суффиксом.
ответ
{
  "data": {
    "payment_id": "pay_01HZX...",      // ← идентификатор для проверки статуса
    "external_id": "order-42",         // номер заказа из запроса
    "asset_id": "btc",
    "expected_amount": "0.001",
    "credit_usdt": "59.4",
    "status": "awaiting_deposit",      // ожидание перевода
    "channel": "api",
    "deposit_address": "bc1qexampledepositaddress000000000",
                                       // ← адрес для отображения клиенту
    "expires_at": "2026-08-11T18:00:00+00:00",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}

Если сеть требует memo, в ответе будет поле deposit_memo. Перевод без memo не будет зачислен — отображайте оба поля.

4
Ожидание оплаты

Запрашивайте статус платежа каждые 10–30 секунд. Статус проходит цепочку awaiting_deposit → detected → processing → confirmed.

запрос
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

GET /payments/{payment_id}
Подставьте payment_id из предыдущего ответа.
Когда
Рекомендуемый интервал — 10–30 секунд. Лимит — 120 запросов в минуту на ключ, чаще отправлять не следует.
ответ
{
  "data": {
    "payment_id": "pay_01HZX...",
    "status": "detected",              // перевод обнаружен в сети,
                                       // подтверждений пока недостаточно
    "asset_id": "btc",
    "expected_amount": "0.001",
    "actual_amount": "0.001",          // ← фактически поступившая сумма
    "deposit_address": "bc1qexampledepositaddress000000000",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}
5
Платёж подтверждён

Статус confirmed означает, что средства зачислены на баланс. Заказ можно закрывать.

запрос
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

status: confirmed
Финальный статус, далее не изменяется.
credited_amount
Фактически зачисленная сумма USDT. При точной оплате совпадает с credit_usdt.
ответ
{
  "data": {
    "payment_id": "pay_01HZX...",
    "external_id": "order-42",
    "status": "confirmed",             // ← зачисление выполнено
    "asset_id": "btc",
    "expected_amount": "0.001",
    "actual_amount": "0.001",          // поступила ожидаемая сумма
    "credit_usdt": "59.4",
    "credited_amount": "59.4",         // ← сумма зачисления на баланс
    "fee": "0.6",                      // комиссия, уже учтена в credited_amount
    "deposit_address": "bc1qexampledepositaddress000000000",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}

Для сверки используйте credited_amount, а не expected_amount: при недоплате значения различаются.

Важно

  • Не создавайте платёж заранее: фиксация курса действует 10 минут, адрес выдаётся на конкретную сумму.
  • Не закрывайте заказ по статусу detected: средства ещё не зачислены.

Клиент платит сразу в USDT

Последовательность та же, что в сценарии с BTC; отличается только asset_id. Ниже — сокращённый пример.

1
Фиксируем сумму

Конвертация не выполняется, поэтому credit_usdt почти равен сумме за вычетом комиссии.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments/credit-preview \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -d '{"asset_id":"usdt","amount":"100"}'

Что здесь важно

asset_id: "usdt"
Единственное отличие от сценария с BTC.
amount: "100"
Клиент отправляет 100 USDT в сети TRON.
ответ
{
  "data": {
    "credit_lock_id": "b2c3d4e5-...",
    "asset_id": "usdt",
    "expected_amount": "100",
    "credit_usdt": "99",         // ← 1 USDT — комиссия
    "expires_at": "2026-08-11T17:20:00+00:00"
  }
}
2
Создаём платёж

Далее — тот же шаг. Адрес в сети TRON (начинается с T).

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payments \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-43-payment" \
  -d '{"credit_lock_id":"b2c3d4e5-...","external_id":"order-43"}'

Что здесь важно

Idempotency-Key
Для нового заказа используйте новый Idempotency-Key. Ключ от order-42 повторно не применяйте.
ответ
{
  "data": {
    "payment_id": "pay_01HZY...",
    "external_id": "order-43",
    "asset_id": "usdt",
    "expected_amount": "100",
    "credit_usdt": "99",
    "status": "awaiting_deposit",
    "deposit_address": "TXYZexampletronaddress000000000",
                                 // ← TRON-адрес, сеть TRC20
    "expires_at": "2026-08-11T18:00:00+00:00"
  }
}

Укажите клиенту сеть TRC20. Перевод USDT в другой сети на этот адрес не будет зачислен.

Клиент прислал меньше, чем нужно

Ожидалось 0.001 BTC, поступило 0.0009 — например, кошелёк клиента удержал комиссию сети из суммы.

1
Проверяем платёж

Если поступило меньше expected_amount, но сумма достаточна для зачисления, статус будет confirmed_underpaid, а credited_amount окажется ниже расчётного. При слишком малой сумме зачисление не выполняется.

запрос
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

Что произошло
Сверяйте credited_amount, а не credit_usdt. Первое — фактически зачисленная сумма, второе — расчёт при точной оплате.
ответ
{
  "data": {
    "payment_id": "pay_01HZX...",
    "external_id": "order-42",
    "status": "confirmed_underpaid",   // ← недоплата, но деньги зачислены
    "asset_id": "btc",
    "expected_amount": "0.001",        // ожидаемая сумма
    "actual_amount": "0.0009",         // фактически поступившая сумма
    "credit_usdt": "59.4",             // расчёт при точной оплате
    "credited_amount": "53.4",         // ← фактически зачисленная сумма
    "fee": "0.6",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}

Дальнейшие действия — на вашей стороне: запросить доплату, исполнить заказ частично или отменить его. API возвращает фактически зачисленную сумму.

Важно

  • Не определяйте факт оплаты сравнением actual_amount и expected_amount. Сверяйте credited_amount с требуемой суммой в USDT.

Клиент не заплатил вовремя

Срок expires_at истёк, перевод не поступал. Платёж переходит в expired; отслеживание адреса при этом продолжается.

1
Статус expired

Окно оплаты истекло. Адрес продолжает отслеживаться ещё около 6 часов.

запрос
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

status: expired
Окно оплаты истекло.
credited_amount
Средства ещё не поступали.
ответ
{
  "data": {
    "payment_id": "pay_01HZX...",
    "status": "expired",               // окно истекло
    "asset_id": "btc",
    "expected_amount": "0.001",
    "deposit_address": "bc1qexampledepositaddress000000000",
    "expires_at": "2026-08-11T18:00:00+00:00"
    // полей actual_amount и credited_amount нет — перевод не поступал
  }
}
2
Клиент заплатил с опозданием

Если перевод поступит в дополнительном окне наблюдения, платёж перейдёт в confirmed. Не удаляйте заказ по expired: отметьте его как просроченный и продолжайте проверять статус.

запрос
curl -sS https://snapex.pro/api/v1/merchant/payments/pay_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

expired → confirmed
Штатная ситуация: перевод поступил после expires_at.
ответ
{
  "data": {
    "payment_id": "pay_01HZX...",
    "status": "confirmed",             // ← зачисление после expires_at
    "actual_amount": "0.001",
    "credited_amount": "59.4",
    "created_at": "2026-08-11T17:00:00+00:00"
  }
}

Важно

  • Не выдавайте новый адрес сразу после expired: клиент мог уже отправить перевод на прежний адрес. Мы ожидаем перевод ещё 6 часов.

Инвойсы

Счёт в USD со страницей оплаты. Клиент выбирает сеть, мерчант получает кредит в USDT.

Счёт на 15 USD, клиент платит USDT

Заказ order-15. Мерчант выставляет счёт в долларах и отдаёт клиенту ссылку на страницу оплаты.

1
Создайте инвойс

Счёт фиксирует сумму в USD, кто платит комиссию, и выдаёт pay_url. Адрес ещё не создан.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/invoices \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: order-15-invoice" \
  -d '{
    "amount_usd": "15.00",
    "fee_on": "merchant",
    "external_id": "order-15"
  }'

Что здесь важно

amount_usd
Сумма заказа строкой. Не передавайте одновременно amount и amount_atomic — здесь только USD.
fee_on
merchant — комиссию платите вы, клиент видит 15.00. customer — комиссию платит клиент.
Idempotency-Key
Обязателен. Повтор с тем же ключом вернёт тот же инвойс.
ответ
{
  "data": {
    "invoice_id": "INV-8K2M1Q0P",
    "amount_usd": "15.00",
    "fee_on": "merchant",
    "charge_usd": "15.00",
    "credit_usd": "13.85",
    "status": "open",
    "pay_url": "https://pay.snapex.pro/0123456789abcdef0123456789abcdef",
    "methods": [
      { "asset_id": "usdt.trc20", "symbol": "USDT", "network": "TRC20" },
      { "asset_id": "usdt.erc20", "symbol": "USDT", "network": "ERC20" },
      { "asset_id": "usdt.bep20", "symbol": "USDT", "network": "BEP20" }
    ]
  }
}

Ключ должен иметь invoices.write или payments.write. Тестовый контур: environment=test.

2
Клиент выбирает сеть

На этом шаге выдаётся уникальный адрес. Способ можно сменить, пока перевод не поступил.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/invoices/INV-8K2M1Q0P/methods \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-15-trc20" \
  -d '{"asset_id":"usdt.trc20"}'

Что здесь важно

asset_id: "usdt.trc20"
Код из methods: usdt.trc20, usdt.erc20 или usdt.bep20. Другие монеты — из того же списка.
payment.address
Покажите клиенту только этот адрес и сумму. Memo и Tag не используются.
ответ
{
  "data": {
    "invoice_id": "INV-8K2M1Q0P",
    "status": "awaiting",
    "selected_asset_id": "usdt.trc20",
    "payment": {
      "asset_id": "usdt.trc20",
      "amount": "15 USDT",
      "address": "TExampleUniqueAddressForThisAttempt",
      "status": "awaiting"
    }
  }
}

Можно не вызывать этот метод: клиент сам выберет сеть на pay_url.

3
Инвойс оплачен

Первый завершённый перевод закрывает счёт. Повторный перевод на другой адрес зачисляется отдельно.

запрос
curl -sS https://snapex.pro/api/v1/merchant/invoices/INV-8K2M1Q0P \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

status: paid
Финальный статус. Заказ можно закрывать.
invoice.paid
Вебхук invoice.paid придёт на URL из кабинета. Проверка подписи — в разделе «Вебхуки».
ответ
{
  "data": {
    "invoice_id": "INV-8K2M1Q0P",
    "status": "paid",
    "credit_usd": "13.85",
    "paid_at": "2026-09-25T12:00:10+00:00"
  }
}

Недоплата не закрывает счёт: статус underpaid, тот же адрес ждёт доплату (USDT — 24 часа). Если остаток не пришёл вовремя, счёт завершается статусом partially_paid.

Важно

  • Не называйте внутренние маршруты в интерфейсе клиента. На странице оплаты только адрес, сумма и сеть.
  • Возврата через API нет. Второй завершённый перевод — отдельное зачисление, не отмена первого.

Выплаты

Исходящие переводы со своего USDT-баланса.

Отправляем клиенту 15 USDT в BTC

Поле amount — сумма списания с вашего баланса, а не сумма к получению. Сумму получателя возвращает preview.

1
Проверьте доступный баланс

Используйте available, а не сумму available и held. Held — резерв под незавершённые выплаты, к расходу недоступен.

запрос
curl -sS https://snapex.pro/api/v1/merchant/balances \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

available
Доступный остаток для списания.
held
Резерв под выплаты в обработке.
ответ
{
  "data": [
    {
      "asset_id": "usdt",
      "available": "74.4",       // ← доступно к списанию
      "available_atomic": "74400000",
      "held": "0",               // резерв отсутствует
      "held_atomic": "0"
    }
  ]
}
2
Рассчитайте выплату

Preview возвращает сумму BTC к получению за 15 USDT и размер комиссии. Списание на этом шаге не выполняется: фиксируется только расчёт.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: wd-77-preview" \
  -d '{
    "destination_asset_id": "btc",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "amount": "15"
  }'

Что здесь важно

amount: "15"
Сумма списания USDT с баланса. Это не сумма к зачислению получателю.
destination_asset_id
Актив получателя. Код из GET /assets.
destination_network
Сеть получателя. Для BTC — bitcoin. Значение берите из поля network в GET /assets.
destination_address
Адрес получателя. Проверьте его до отправки: отменить перевод нельзя.
Idempotency-Key
Обязателен. Используйте отдельный ключ для preview.
ответ
{
  "data": {
    "preview_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
                                     // ← идентификатор для следующего шага
    "from_asset_id": "usdt",
    "from_amount": "15",             // сумма списания
    "to_asset_id": "btc",
    "to_amount": "0.000221",         // ← сумма к получению
    "fee_amount": "0.000019",        // комиссия в BTC, уже учтена
    "expires_at": "2026-08-11T17:20:00+00:00"
                                     // ← срок исполнения preview
  }
}

Отображайте пользователю to_amount как сумму к получению. Если сумма не подходит, не исполняйте preview: по истечении expires_at он станет недействительным.

3
Исполните выплату

На этом шаге выполняется списание и отправка. Адрес и сеть необходимо повторить в запросе — это защита от подмены между шагами.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Idempotency-Key: wd-77-execute" \
  -d '{
    "preview_id": "c3d4e5f6-7890-abcd-ef12-34567890abcd",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "external_id": "wd-77"
  }'

Что здесь важно

Idempotency-Key: wd-77-execute
Новый ключ, отличный от ключа preview. Повтор ключа preview вернёт 409.
destination_network / address
Должны в точности совпадать с preview, иначе вернётся 422.
external_id
Номер операции на вашей стороне для последующего поиска.
ответ
{
  "data": {
    "payout_id": "payout_01HZX...",  // ← идентификатор для отслеживания
    "external_id": "wd-77",
    "asset_id": "usdt",
    "debit_amount": "15",            // списано с баланса
    "destination_asset_id": "btc",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "destination_amount": "0.000221",// сумма к получению
    "fee": "0.000019",
    "status": "reserved",            // средства зарезервированы, отправка начата
    "channel": "api",
    "created_at": "2026-08-11T17:10:00+00:00"
  }
}
4
Дождитесь завершения

Цепочка статусов: reserved → submitting → processing → completed. Запрашивайте статус каждые 15–30 секунд.

запрос
curl -sS https://snapex.pro/api/v1/merchant/payouts/payout_01HZX... \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

status: completed
Средства доставлены получателю. Финальный статус.
status: failed
Перевод не выполнен; резерв, как правило, возвращается на баланс.
status: manual_review
Результат неоднозначен. Дождитесь разбора, повторную выплату не создавайте.
ответ
{
  "data": {
    "payout_id": "payout_01HZX...",
    "external_id": "wd-77",
    "debit_amount": "15",
    "destination_amount": "0.000221",
    "status": "completed",           // ← доставка завершена
    "created_at": "2026-08-11T17:10:00+00:00"
  }
}

Важно

  • Поле amount — сумма списания, а не сумма к получению.
  • Для выплат действует отдельный лимит: 5 запросов в минуту и 30 в час. Не вызывайте preview в цикле.
  • Если срок preview истёк (expires_at), POST /payouts вернёт 422. Запросите новый preview.

Отправляем USDT на внешний TRON-кошелёк

Отличие в том, что destination_asset_id также равен usdt. Комиссия в этом случае минимальна.

1
Рассчитайте выплату

Актив списания и получения совпадает, поэтому to_amount почти равен amount за вычетом сетевой комиссии.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-78-preview" \
  -d '{
    "destination_asset_id": "usdt",
    "destination_network": "TRON",
    "destination_address": "TXYZexampletronaddress000000000",
    "amount": "50"
  }'

Что здесь важно

destination_network: "TRON"
Значение берите из поля network в GET /assets; регистр учитывается.
amount: "50"
С баланса будет списано 50 USDT.
ответ
{
  "data": {
    "preview_id": "d4e5f6a7-...",
    "from_asset_id": "usdt",
    "from_amount": "50",           // сумма списания
    "to_asset_id": "usdt",
    "to_amount": "48.9",           // ← сумма к получению (за вычетом комиссии сети)
    "fee_amount": "1.1",
    "expires_at": "2026-08-11T17:25:00+00:00"
  }
}
2
Исполните выплату

Далее — та же последовательность, что в предыдущем сценарии.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-78-execute" \
  -d '{
    "preview_id": "d4e5f6a7-...",
    "destination_network": "TRON",
    "destination_address": "TXYZexampletronaddress000000000",
    "external_id": "wd-78"
  }'
ответ
{
  "data": {
    "payout_id": "payout_01HZZ...",
    "debit_amount": "50",
    "destination_asset_id": "usdt",
    "destination_amount": "48.9",
    "status": "reserved",
    "created_at": "2026-08-11T17:26:00+00:00"
  }
}

Важно

  • Убедитесь, что адрес получателя относится к сети TRON. Перевод в другую сеть не выполняется: запрос будет отклонён либо средства не поступят.

Не хватает баланса на выплату

На балансе 10 USDT, запрошена выплата на 15.

1
Preview отклоняет запрос

Баланс проверяется на этапе расчёта, до списания. Ошибку можно сразу показать пользователю.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts/preview \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-79-preview" \
  -d '{
    "destination_asset_id": "btc",
    "destination_network": "bitcoin",
    "destination_address": "bc1q…",
    "amount": "15"
  }'

Что здесь важно

HTTP 422
Отказ по бизнес-правилу. Формат ответа совпадает с ошибками валидации.
details.amount
Ошибка привязана к полю — её можно отобразить в форме.
ответ
{
  "error": {
    "code": "validation_error",
    "message": "The request is invalid.",
    "details": {
      "amount": ["Недостаточно средств на балансе."]
                              // ← причина отказа относится к полю amount
    }
  }
}

Резерв не создаётся. Повтор с тем же Idempotency-Key безопасен; предварительно пополните баланс.

Важно

  • Учитывайте held: если часть средств зарезервирована под другую выплату, available будет меньше ожидаемого.

Служебные сценарии

История платежей

Ledger — журнал операций: зачисления, списания и резервы фиксируются отдельными записями.

1
Получите историю постранично

Записи возвращаются от новых к старым. За один запрос — до 100 записей, следующая страница — по курсору.

запрос
curl -sS "https://snapex.pro/api/v1/merchant/transactions?limit=50" \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

limit=50
Число записей в ответе: от 1 до 100, по умолчанию 25.
status=payment
Необязательный фильтр по типу записи.
ответ
{
  "data": [
    {
      "id": "124",
      "direction": "credit",          // зачисление
      "type": "payment",              // источник — пополнение
      "amount": "59.4",
      "asset_id": "usdt",
      "channel": "api",
      "occurred_at": "2026-08-11T17:05:00+00:00"
    },
    {
      "id": "125",
      "direction": "reserve",         // резерв под выплату
      "type": "payout",
      "amount": "15",
      "asset_id": "usdt",
      "occurred_at": "2026-08-11T17:10:00+00:00"
    }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6MTI1fQ",  // ← значение для параметра cursor
    "per_page": 50
  }
}
2
Следующая страница

Если next_cursor равен null, записей больше нет.

запрос
curl -sS "https://snapex.pro/api/v1/merchant/transactions?limit=50&cursor=eyJpZCI6MTI1fQ" \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Accept: application/json"

Что здесь важно

cursor
Значение meta.next_cursor из предыдущего ответа. Произвольное значение недопустимо.
ответ
{
  "data": [ /* следующие записи */ ],
  "meta": {
    "next_cursor": null,              // ← последняя страница
    "per_page": 50
  }
}

Важно

  • Значения direction reserve и release — резерв и снятие резерва, а не исходящий перевод. Не учитывайте их как расход.

Не пришёл ответ — повторите тот же запрос

1
Повторите тот же запрос

Используйте тот же Idempotency-Key и те же параметры. Сервер обработает запрос как ту же операцию.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-77-execute" \
  -d '{
    "preview_id": "c3d4e5f6-...",
    "destination_network": "bitcoin",
    "destination_address": "bc1qexampledestinationaddress000000",
    "external_id": "wd-77"
  }'

Что здесь важно

Idempotency-Key
Новый ключ создаст новую выплату.
HTTP 200 / 201
Операция уже существовала, в ответе возвращён исходный объект.
ответ
{
  "data": {
    "payout_id": "payout_01HZX...",   // ← тот же идентификатор, что в первом ответе
    "external_id": "wd-77",
    "debit_amount": "15",
    "status": "processing",           // статус мог измениться за время ожидания
    "created_at": "2026-08-11T17:10:00+00:00"
  }
}

HTTP 201 — операция создана в этом запросе. HTTP 200 — операция уже существовала. Оба кода означают успех.

2
Если параметры изменились

Повтор того же ключа с другой суммой или адресом будет отклонён. Это защита от расхождения параметров.

запрос
curl -sS -X POST https://snapex.pro/api/v1/merchant/payouts \
  -H "Authorization: Bearer smk_abcdefghij_вашключ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: wd-77-execute" \
  -d '{
    "preview_id": "ДРУГОЙ-preview",
    "destination_network": "bitcoin",
    "destination_address": "bc1qдругойадрес"
  }'

Что здесь важно

HTTP 409
Конфликт: ключ уже связан с другой операцией.
Что делать
Если это новая выплата, укажите новый Idempotency-Key.
ответ
{
  "error": {
    "code": "conflict",
    "message": "Idempotency-Key was used with different parameters."
  }
}

Важно

  • Сформируйте ключ один раз и сохраните его вместе с заказом. Генерация нового ключа перед каждой отправкой отключает защиту от дублей.
  • При HTTP 429 повторите запрос с тем же ключом после паузы в несколько секунд.

Вебхуки

Когда меняется инвойс, депозит, выплата, конвертация или баланс, SnapEX отправляет POST-запрос на ваш URL. Вебхук — сигнал, а не источник истины: проверяйте подпись, а для важных решений перечитывайте объект через API.

Настройка

  1. Укажите URL в кабинете

    Кабинет партнёра → Инвойсы → карточка «Webhook»: введите URL и нажмите «Сохранить». Один URL на аккаунт, на него приходят все типы событий из таблицы ниже.

  2. Сохраните секрет подписи

    При первом сохранении кабинет один раз показывает секрет whsec_…. Храните его только на своём сервере. При смене URL секрет остаётся прежним.

  3. Требования к URL

    Абсолютный https:// адрес с публичным хостом: без login:password в URL, не localhost, хост не должен указывать на частный или зарезервированный IP. URL проверяется при сохранении и повторно перед каждой попыткой доставки.

Что приходит

Заголовок Что произошло
Webhook-Id Уникальный id события (UUID), совпадает с id в теле. Не меняется при повторах и ручной переотправке — по нему убирайте дубли.
Webhook-Timestamp Unix-время в секундах, когда отправлена эта попытка. У каждого повтора свежее значение и свежая подпись.
Webhook-Signature v1= и hex (в нижнем регистре) от HMAC-SHA256 строки «{Webhook-Timestamp}.{сырое тело}». Ключ — секрет целиком, вместе с префиксом whsec_.
Content-Type Всегда application/json. Тело — JSON в UTF-8.
запрос
POST /hooks/snapex HTTP/1.1
Content-Type: application/json
Webhook-Id: 6f1c2a8e-3b4d-4f5a-9c7e-1a2b3c4d5e6f
Webhook-Timestamp: 1790337610
Webhook-Signature: v1=2d711642b726b04401627ca9fbac32f5c8530fb1903cc4db02258717921a4881
тело
{
  "id": "6f1c2a8e-3b4d-4f5a-9c7e-1a2b3c4d5e6f",   // = Webhook-Id
  "type": "invoice.paid",                          // тип события, см. таблицу ниже
  "created_at": "2026-09-25T12:00:10+00:00",       // когда произошло событие
  "data": {                                        // объект на момент события
    "invoice_id": "INV-8K2M1Q0P",
    "environment": "live",
    "amount_usd": "15.00",
    "charge_usd": "15.00",
    "credit_usd": "13.85",
    "status": "paid",
    "external_id": "order-15",
    "paid_at": "2026-09-25T12:00:10+00:00"
  }
}

Состав data зависит от типа: invoice.* — инвойс в том же виде, что GET /invoices/{id} (attempts пустой, текущая попытка — в payment); payment.* — депозит (payment_id, external_id, status, суммы); payout.* — выплата (payout_id, external_id, status, суммы); conversion.* — конвертация (conversion_id, status, суммы); balance.changed — asset_id, entry_type, available_atomic и held_atomic после изменения.

Типы событий

Событие Что произошло
invoice.created Инвойс создан, способ оплаты ещё не выбран (open).
invoice.awaiting Выбран способ оплаты, выдан адрес (awaiting).
invoice.underpaid Пришла часть суммы, тот же адрес ждёт доплату (underpaid).
invoice.paid Полная сумма оплачена и зачислена (paid, финальный). Единственный сигнал для отгрузки заказа.
invoice.partially_paid Клиент оплатил только часть, окно доплаты закончилось, полученная часть зачислена вам (partially_paid, финальный).
invoice.expired Срок счёта истёк без оплаты (expired, финальный).
invoice.extra_payment После оплаты счёта пришёл ещё один перевод; он зачислен отдельно.
payment.created Создан депозит.
payment.detected Перевод виден в сети, но ещё не зачислен.
payment.credited Депозит зачислен (один из статусов confirmed*).
payment.expired Окно оплаты депозита истекло.
payout.created Создана выплата, средства зарезервированы.
payout.processing Выплата отправляется.
payout.completed Средства доставлены получателю.
payout.failed Выплата не прошла.
conversion.completed Конвертация завершена.
conversion.failed Конвертация не прошла полностью или частично.
balance.changed Записано движение по балансу: зачисление, резерв или списание.

Отгружайте заказ только по invoice.paid — он отправляется только когда оплачена полная сумма. invoice.underpaid и invoice.partially_paid означают, что сумма неполная.

Инвойсы с environment=test — симуляция: они проходят те же статусы и шлют те же вебхуки (data.environment = "test"), но никогда не меняют реальный баланс.

Доставка и повторы

  • Успех — любой ответ 2xx. Любой другой код, таймаут или сетевая ошибка — неудача. Тело ответа не анализируется.
  • Таймауты: 5 с на соединение, 15 с на весь запрос.
  • Редиректы не выполняются: 301/302 считается неудачей. Указывайте конечный URL.
  • До 5 попыток на событие: первая, затем повторы примерно через 10 с, 1 мин, 5 мин и 15 мин после каждой неудачи. После пятой неудачи автоматическая доставка прекращается.
  • В той же карточке — «Последние доставки»: событие, HTTP-код или ошибка, время и номер попытки. Кнопка «Повторить» отправляет событие ещё раз с тем же Webhook-Id.
  • Доставка «как минимум один раз»: одно событие может прийти несколько раз, а события — не по порядку (повтор старого события может прийти после нового).

Проверка подписи

  1. Прочитайте сырое тело запроса как байты, до разбора JSON. Пересериализованный JSON с подписью не совпадёт.
  2. Посчитайте HMAC-SHA256 от «{Webhook-Timestamp}.{сырое тело}» с ключом — вашим секретом whsec_…, закодируйте в hex.
  3. Сравните «v1=» + hex с заголовком Webhook-Signature функцией постоянного времени: hash_equals, crypto.timingSafeEqual, hmac.compare_digest.
  4. Отклоняйте Webhook-Timestamp, отличающийся от ваших часов больше чем на 5 минут (рекомендация). У повторов свежая метка времени, поэтому они не пострадают.
PHP
<?php
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // whsec_… из кабинета
$body = file_get_contents('php://input');  // сырое тело, до разбора JSON
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';

$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
if (! ctype_digit($timestamp)
    || abs(time() - (int) $timestamp) > 300
    || ! hash_equals($expected, $signature)) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
// пропустите, если этот Webhook-Id уже обработан; тяжёлую работу — в очередь
http_response_code(200);

Чек-лист обработчика

  • Проверяйте подпись по сырому телу сравнением постоянного времени; при несовпадении отвечайте 400.
  • Отклоняйте запросы с Webhook-Timestamp старше 5 минут.
  • Храните обработанные Webhook-Id и игнорируйте повторы. Не полагайтесь на порядок: сравнивайте статус в событии с тем, что у вас уже записано.
  • Отвечайте 2xx быстро, заметно быстрее 15 секунд, а тяжёлую работу делайте асинхронно.
  • После вебхука можно перечитать объект через API для подтверждения: GET /invoices/{id}, GET /payments/{id}, GET /payouts/{id}.

Статусы

Бейдж «финал» означает, что статус больше не изменится. Единственное исключение — expired у пополнений: опоздавший перевод всё ещё может прийти.

Пополнения

Статус Что произошло Что делать
awaiting_deposit Адрес выдан, ожидается перевод Отобразите клиенту адрес и сумму
detected Перевод обнаружен в сети, зачисление ещё не выполнено Ожидайте. Заказ не закрывайте
processing Выполняется конвертация в USDT Ожидайте
confirmed Поступила ожидаемая сумма, средства зачислены Закройте заказ финал
confirmed_underpaid Поступило меньше ожидаемого, в допустимых пределах Сверьте credited_amount и определите, достаточна ли сумма финал
confirmed_overpaid Поступило больше ожидаемого, зачислено по credit-preview Закройте заказ. По превышению обратитесь в поддержку финал
confirmed_mismatch Сохранён для совместимости На новых платежах не устанавливается финал
manual_review Требуется ручная проверка Дождитесь разбора, повторный платёж не создавайте
expired Срок оплаты истёк, адрес отслеживается ещё ~6 ч Не удаляйте заказ, продолжайте проверять статус
failed Сбой на стороне провайдера Создайте новый платёж финал
cancelled Платёж отменён Создайте новый платёж финал
refunded Возврат выполнен провайдером Сверьте баланс; при расхождениях обратитесь в поддержку финал

Инвойсы

Статус Что произошло Что делать
open Счёт создан, способ оплаты ещё не выбран Отдайте клиенту pay_url или выберите asset_id сами
awaiting Адрес выдан, ожидается перевод Показывайте адрес и сумму, опрашивайте статус
underpaid Пришла часть суммы, адрес тот же Попросите доплатить остаток на этот адрес
paid Первый полный перевод зачислен Закройте заказ финал
partially_paid Клиент оплатил только часть, окно доплаты закончилось, полученная часть зачислена вам Не отгружайте как оплаченный заказ; остаток решайте с клиентом финал
expired Срок счёта истёк Создайте новый инвойс финал

Выплаты

Статус Что произошло Что делать
reserved Средства зарезервированы под выплату Ожидайте
submitting Заявка передана провайдеру Ожидайте
processing Провайдер отправляет перевод в сеть Ожидайте
completed Средства доставлены получателю Завершено финал
manual_review Результат неоднозначен Повторную выплату не создавайте, дождитесь разбора
failed Перевод не выполнен, резерв, как правило, возвращён Проверьте баланс; при необходимости повторите с новым Idempotency-Key финал

Ошибки

Успешный ответ всегда лежит в data. Ошибка — в error. Больше форматов нет.

как выглядит ошибка
{
  "error": {
    "code": "validation_error",     // машиночитаемый код, см. таблицу ниже
    "message": "The request is invalid.",
    "details": {                    // есть не всегда
      "destination_address": ["The destination address field is required."]
                                    // ключ — имя поля, которое не прошло
    }
  }
}
HTTP code Когда Что делать
401 unauthorized Ключ не передан или неверный Проверьте заголовок Authorization: Bearer smk_… целиком, без пробелов и переносов
401 key_inactive Ключ отозван Предыдущий ключ отозван. Используйте актуальный ключ из кабинета
401 key_expired У ключа истёк срок Выпустите новый ключ
404 not_found Идентификатор не найден или принадлежит другому мерчанту Используйте payment_id или payout_id из ответа вашего запроса
409 conflict Idempotency-Key занят другой операцией или external_id уже использован Для новой операции укажите новый ключ. Для повтора — тот же ключ и те же параметры
422 validation_error Некорректные поля, истёкший credit-preview, недостаточно средств или отсутствует Idempotency-Key Причина указана в details: поле и описание
429 rate_limited Превышен лимит запросов Повторите запрос позже с тем же Idempotency-Key
500 internal_error Внутренняя ошибка сервера Повторите позже с тем же Idempotency-Key. При устойчивой ошибке обратитесь в поддержку и укажите X-Request-Id