Перейти к содержанию

Балансы и выводы

Деньги, свипнутые с инвойса или статического кошелька, попадают на ваш баланс за вычетом комиссии мерчанта и сетевой комиссии за их перевод. Вывод отправляет их оттуда на адрес, который укажете вы.

Балансы

curl https://api.pay.nullswap.com/api/v1/merchant/balances \
  -H "x-api-key: sk_live_..."
{
  "items": [
    {
      "tokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
      "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
      "currency": "USDT",
      "decimals": 6,
      "amount": "125000000",
      "balanceId": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c"
    }
  ]
}

Обратите внимание на конверт: { "items": [...] }, а не { "data": [...], "pagination": {...} }, как в остальных списках. Ни пагинации, ни фильтров здесь нет — вы получаете все свои строки сразу.

Одна строка — это один токен. Токена, в котором вам ни разу не платили, в ответе обычно просто нет, но строка может показывать и "0" — например, после того как вы вывели всё, — поэтому отсутствие строки и "0" обрабатывайте одинаково. Баланс в токене, которого больше нет в /api/v1/tokens, не показывается.

Поле Значение
tokenId, chainId Токен и сеть, в которой уходит его вывод
currency Символ валюты токена
decimals Масштаб amount
amount Сколько у вас есть, в базовых единицах. Вывести можно всё
balanceId Наш идентификатор этого баланса. Назовите его поддержке

Весь amount можно вывести. На баланс свипаются только деньги, которые подтвердились и прошли скрининг; средства, не прошедшие скрининг, до баланса не доходят и остаются на депозитном или статическом адресе, куда пришли. Смотрите их по платежам: через amlStatus у платежей по инвойсу и settlement: WITHHELD у платежей статических кошельков.

Оплачено — ещё не значит на балансе

Платёж зачисляется сюда, только когда его свипнули. Между invoice.paid и зачислением есть свип в сети, а у инвойса, созданного с autoSweep: false, зачисления не будет вовсе, пока вы не свипнете его сами.

amount — строка с целым числом базовых единиц; чтобы показать её человеку, разделите на 10^decimals. И, как всегда, складывайте суммы только внутри одного tokenId — см. соглашения.

Статус message Причина
502 INTERNAL_ERROR Не удалось прочитать баланс. Можно повторить
502 INTERNAL_ERROR Не удалось найти токены. Можно повторить

Комиссия мерчанта

С каждого мерчанта мы берём комиссию с его платежей: ставку в базисных пунктах, которую показывает поле feePercentage в GET /api/v1/merchant. Её устанавливаем мы, и API-ключ никогда не может её изменить. Комиссия удерживается, когда свип зачисляется на ваш баланс: из того, что пришло, комиссия остаётся у нас, а остальное зачисляется на баланс.

fee      = floor(amount × feePercentage / 10000)
credited = amount − fee − network fee

amount — то, что доставил свип, в базовых единицах токена, в котором он зачислен. Комиссия округляется вниз до целой базовой единицы, так что остаток от округления всегда остаётся у вас. При "0" комиссия мерчанта не удерживается.

Сетевая комиссия — это стоимость перевода денег при свипе токена, оценённая в зачисленном токене и удерживаемая вместе с комиссией мерчанта. Она равна нулю при свипе нативной монеты сети, в UTXO-сетях и в Monero, где свип сам оплачивает себя из того, что переводит. Отдельного поля у неё нет: это разница между amount − fee и credited.

Каждый свип фиксирует ставку в момент, когда он открыт. Изменение ставки касается свипов, открытых после него, и никогда — свипа, который уже в пути.

Свип в SWEPT зачисляет не свой amount

Баланс вырастает на credited, а не на amount, а при свипе токена — и не на amount − fee. Сверяйте свип с балансом по credited из списка ниже и никогда — по собственным цифрам свипа.

Комиссия за вывод — отдельный платёж, который берётся при выводе. Ваши балансы уже за вычетом всех комиссий мерчанта и сетевых комиссий свипов.

Список удержанных комиссий

curl "https://api.pay.nullswap.com/api/v1/merchant/fees?settledFrom=2026-01-01T00:00:00.000Z&settledTo=2026-02-01T00:00:00.000Z&limit=50" \
  -H "x-api-key: sk_live_..."
Параметр Тип Обязателен Примечания
chainId uuid нет Повторяемый. Только свипы в этих сетях
tokenId uuid нет Повторяемый. Только свипы этих токенов
settledFrom ISO-8601 нет Только свипы, зачисленные в этот момент или позже. Включительно
settledTo ISO-8601 нет Только свипы, зачисленные раньше этого момента. Не включительно
offset, limit integer нет См. пагинацию. Передавайте limit
order ASC | DESC нет По времени зачисления. По умолчанию DESC, сначала новые
{
  "data": [
    {
      "id": "0b1c2d3e-4f50-4617-8a29-3b4c5d6e7f80",
      "merchantId": "9f1d2c3b-4a5e-4f60-8b71-2c3d4e5f6a7b",
      "walletId": "7e2a9c14-8b3d-4c56-a789-0f1e2d3c4b5a",
      "invoiceId": "b2f4a6c8-0d1e-4f23-9a45-6b7c8d9e0f11",
      "staticWalletId": null,
      "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
      "tokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
      "transferRequestId": "4d5e6f70-8192-4a3b-9c4d-5e6f708192a3",
      "amount": "25500000",
      "credited": "25117500",
      "fee": "382500",
      "feePercentage": "150",
      "feeUsd": "382576500000000000",
      "settledAt": "2026-01-01T12:14:00.000Z",
      "creditedTokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
      "dstTokenId": null,
      "amountIn": null,
      "amountOut": null,
      "conversionSkipReason": null
    }
  ],
  "pagination": { "offset": 0, "limit": 50, "total": 1, "order": "DESC" }
}

Одна строка — один зачисленный свип. В списке каждый свип, зачисленный на ваш баланс с тех пор, как появилась комиссия мерчанта, в том числе свип по нулевой ставке с fee: "0"; свипов, которые ещё в пути, в нём нет.

Поле Значение
id Идентификатор свипа: id свипа из ответа POST /api/v1/wallets/{walletId}/sweep и sweepId платежа статического кошелька
walletId Свипнутый адрес: depositWalletId инвойса или id статического кошелька
invoiceId, staticWalletId Какой это был адрес. Заполнено ровно одно
transferRequestId Наш идентификатор перевода. Назовите его поддержке
tokenId Токен, который свипнули с адреса
creditedTokenId Токен, в котором зачислено на баланс: tokenId либо целевой токен, если свип сконвертировало правило автоконвертации
amount Что пришло, в базовых единицах creditedTokenId
credited Что зачислено на баланс, в базовых единицах creditedTokenId: amount − fee, а при свипе токена — ещё за вычетом сетевой комиссии
fee Удержанная комиссия мерчанта, в базовых единицах creditedTokenId
feePercentage Применённая ставка в базисных пунктах, зафиксированная при открытии свипа
feeUsd fee в USD на момент зачисления, умноженная на 1018. null, пока не оценена
settledAt Когда свип зачислен на баланс
dstTokenId, amountIn, amountOut, conversionSkipReason Автоконвертация свипа, как у самого свипа. null, если правила для него не было

feeUsd нужна только для отчётности. Комиссия, у токена которой в момент зачисления не было цены, читается как null и оценивается позже — по цене того, более позднего момента. Для точных цифр складывайте fee внутри одного tokenId — см. соглашения.

Статус message Причина
400 INVALID_QUERY_PARAMETER chainId или tokenId — не UUID
400 INVALID_DATE_FILTER settledFrom или settledTo не удалось разобрать
401 INVALID_API_KEY x-api-key отсутствует, искажён, неизвестен или отозван
403 CALLER_IP_NOT_ALLOWED Ключ верный, но адреса отправителя нет в его белом списке
403 MERCHANT_BLOCKED Мерчант заблокирован

Создание вывода

curl -X POST https://api.pay.nullswap.com/api/v1/merchant/withdrawals \
  -H "x-api-key: sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
    "tokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
    "to": "0xfedcba0987654321fedcba0987654321fedcba09",
    "amount": "50000000",
    "externalId": "payout-2026-01-14-001"
  }'
Поле Тип Обязательно Примечания
chainId uuid да
tokenId uuid да Должен быть токеном в этой сети
to string да Адрес назначения в нативном формате сети. На RIPPLE — классический адрес r… или X-адрес, который читается как адрес и тег, упакованные в нём
toTag integer | null нет Только RIPPLE: тег назначения получателя, 0–4294967295. Отклоняется на любой другой сети, а также если расходится с тегом X-адреса в to. См. Вывод на RIPPLE
amount string да Базовые единицы. "50000000" — это 50 USDT при 6 decimals
externalId string | null нет, но настоятельно рекомендуется Ваша ссылка, от 1 до 255 символов. Ключ идемпотентности — см. ниже. Без него повтор отправит деньги дважды
subtractFeeFromAmount boolean нет По умолчанию false. Взять комиссию из amount, а не сверху. См. комиссию
maxFee string | null нет Наибольшая комиссия, на которую вы согласны, в базовых единицах tokenId. Если выше — отказ 422 FEE_EXCEEDS_MAX_FEE
{
  "transferRequestId": "9c0d1e2f-3a4b-45c6-8d78-9e0f1a2b3c4d",
  "status": "PENDING",
  "hash": null,
  "amount": "50000000",
  "fee": "1350000"
}

201 значит принято к обработке, а не отправлено. До сети дело ещё не дошло, hash равен null, результат придёт асинхронно. А вот списание уже произошло: сумма и fee снимаются с вашего баланса в момент приёма вывода, а вывод, который баланс не покрывает, сразу отклоняется с 422 INSUFFICIENT_MERCHANT_BALANCE.

Адрес назначения за вас никто не проверяет

Мы не проверяем ни того, что to вообще похож на адрес из chainId, ни того, что этот адрес принадлежит вам. Вывод на опечатанный или чужой адрес — совершенно нормальный, правильно составленный запрос, и мы попробуем его выполнить. Проверяйте адрес у себя и предпочитайте адресную книгу свободному вводу.

Две проверки to всё же проходят до какого-либо списания:

  • Скрининг. Адрес назначения проверяется при создании вывода. Адрес, не прошедший скрининг, отклоняется с 422 RECIPIENT_ADDRESS_BLOCKED; SKIPPED вывод пропускает. Адреса Monero не проверяются.
  • Ваш белый список выводов, если вы его включили. Владелец мерчанта может ограничить выплаты списком доверенных адресов в кабинете, в разделе Настройки → Белые списки. Когда ограничение включено, to, которого нет в списке для этой сети, отклоняется с 422 WITHDRAWAL_ADDRESS_NOT_WHITELISTED — в том числе при вызовах API, — а при пустом списке отклоняется любая выплата. Список ведётся по сетям: адрес, добавленный для одной EVM-сети, в другой доверенным не считается. На RIPPLE запись списка — это адрес вместе с тегом назначения, и выплата совпадает только с записью ровно с её toTag; запись без тега совпадает только с выплатой без тега.

Вывод на RIPPLE

На RIPPLE, то есть в XRP Ledger, большинство бирж и кастодианов принимают деньги на один счёт, общий для всех их клиентов, и различают клиентов по тегу назначения (destination tag). Передайте тег получателя в toTag или передайте в to X-адрес, который вам дали. Выплата на такой счёт без тега дойдёт до биржи, но ни на чей баланс там не попадёт.

Ещё две проверки до какого-либо списания читают счёт получателя из леджера:

  • Обязательный тег. Счёт, который не принимает платежи без тега назначения, при выплате без toTag даёт отказ 422 DESTINATION_TAG_REQUIRED.
  • Счёт, которого ещё нет. Счёт в XRP Ledger создаётся первым платежом на него, и только если этот платёж не меньше базового резерва сети — сейчас это 1 XRP. Выплата на несуществующий счёт меньше этой суммы отклоняется с 422 DESTINATION_NOT_ACTIVATED, а details.minAmount сообщает, сколько нужно, в drops:
{
  "statusCode": 422,
  "message": "DESTINATION_NOT_ACTIVATED",
  "timestamp": "2026-01-14T09:00:00.000Z",
  "details": {
    "minAmount": "1000000"
  }
}

Берите цифру из details.minAmount, а не зашивайте её в код: резерв устанавливает сеть, и она может его изменить. Ни один из этих отказов ничего не списывает, и тот же externalId можно отправить снова, когда запрос исправлен.

Комиссия

Вывод стоит столько, сколько сетевая комиссия за его отправку, и платите вы её в выводимом токене — по текущему курсу этого токена и с небольшим запасом на изменение цены между оценкой и отправкой. fee в ответе — то, что с вас взяли, amount — то, что отправят получателю, оба в базовых единицах tokenId. Других сборов нет, и держать газ не нужно: чтобы вывести токен, баланс в нативной монете сети не требуется.

subtractFeeFromAmount Получатель получит Спишется с баланса
false (по умолчанию) amount amount + fee
true amount − fee amount

true — это способ вывести всё: отправьте весь баланс в amount, и комиссия возьмётся из него. С false вывод всего баланса будет отклонён — на комиссию не хватит.

Комиссия пересчитывается в момент вывода, поэтому может отличаться от любой показанной вам оценки. Передайте maxFee, чтобы ограничить её: вывод, комиссия которого оказалась выше, отклоняется с 422 FEE_EXCEEDS_MAX_FEE, и ничего не списывается.

Вывести можно только токен с известным курсом

Комиссия пересчитывается по курсу токена. Токен, курса которого у нас нет, вывести нельзя: запрос вернёт 422 TOKEN_PRICE_UNAVAILABLE. То же относится к оценке комиссии.

Оценка комиссии

curl "https://api.pay.nullswap.com/api/v1/merchant/withdrawals/fee-quote?chainId=1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10&tokenId=3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908" \
  -H "x-api-key: sk_live_..."
Параметр Тип Обязательно Примечания
chainId uuid да
tokenId uuid да Токен, который вы собираетесь выводить
{
  "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
  "tokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
  "nativeTokenId": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "nativeFee": "2800000",
  "fee": "1350000"
}

fee — сколько взяли бы за вывод tokenId прямо сейчас, в его базовых единицах, с учётом запаса. nativeFee — сетевая стоимость, из которой он пересчитан, в базовых единицах нативной монеты сети nativeTokenId.

Это только оценка. Ничего не резервируется и курс не фиксируется: если комиссия должна остаться в показанных пределах, передайте показанный fee как maxFee при выводе.

Статус message Причина
400 INVALID_QUERY_PARAMETER chainId или tokenId не передан либо не UUID
404 CHAIN_NOT_FOUND Неизвестный chainId
404 TOKEN_NOT_FOUND Неизвестный tokenId либо токен не из этой сети
422 TOKEN_PRICE_UNAVAILABLE Нет курса, чтобы пересчитать комиссию в этот токен
502 INTERNAL_ERROR Не удалось оценить комиссию. Можно повторить

Здесь externalId действительно ключ идемпотентности

Это единственная операция записи в API, где повторный externalId безопасен, а не ошибочен. Отправьте один и тот же дважды с теми же параметрами — повтор после таймаута сокета, задублившаяся задача — и второй вызов вернёт уже созданный вывод, а не сделает второй. Уникальность считается в пределах вашего мерчанта.

externalId необязателен, но не передавайте его, только если никогда не повторяете запросы. Без него каждый вызов — новый вывод: два одинаковых запроса — это две выплаты, а повтор после таймаута, ответа на который вы так и не увидели, отправит деньги дважды.

Тот же externalId с любым изменённым параметром — chainId, tokenId, to, toTag, amount, subtractFeeFromAmount или maxFee — отклоняется с 409 WITHDRAWAL_EXTERNAL_ID_CONFLICT, и ничего не отправляется. Это значит, что ваша система использовала одну ссылку для двух разных выплат; исправьте ссылку, а не повторяйте запрос.

Сравните с инвойсами, где повторный externalId даёт 409. Асимметрия сделана намеренно: дублирующийся инвойс поправим, а дублирующийся вывод отправит деньги дважды.

Всегда передавайте его и делайте воспроизводимым

Стройте его из чего-то устойчивого в вашей собственной системе — из id строки выплаты, а не из метки времени или случайного значения. externalId, который вы не сможете воспроизвести после сбоя, ни от чего не защищает.

Повтор возвращает существующий вывод, чем бы он ни закончился, в том числе FAILED. Чтобы повторить проваленный вывод, отправьте новый externalId. Вывод, отклонённый с 422, не записывается, поэтому тот же externalId можно использовать снова, когда причина устранена. После 5xx externalId остаётся занятым: повтор с ним вернёт то, чем закончилась первая попытка.

Ошибки

Статус message Причина
400 INVALID_REQUEST_BODY Поле отсутствует или имеет неверный формат: externalId передан, но пуст, состоит из пробелов, не является строкой или длиннее 255 символов, chainId или tokenId — не UUID, to пустой, amount — не строка с положительным целым числом, maxFee передан, но это не строка с неотрицательным целым числом, subtractFeeFromAmount — не булево значение, toTag — не целое число 0–4294967295, toTag передан на сети, отличной от RIPPLE, to — X-адрес другой сети или его тег расходится с toTag
403 MERCHANT_BLOCKED Мерчант заблокирован
404 CHAIN_NOT_FOUND Неизвестный chainId
404 TOKEN_NOT_FOUND Неизвестный tokenId либо токен не из этой сети
409 WITHDRAWAL_EXTERNAL_ID_CONFLICT Этот externalId уже использован для вывода с другими параметрами
422 WITHDRAWAL_ADDRESS_NOT_WHITELISTED Ваш белый список выводов включён, а to в нём для этой сети нет
422 INSUFFICIENT_MERCHANT_BALANCE Баланс не покрывает то, что с него спишется, — amount, плюс fee, если не задан subtractFeeFromAmount
422 TOKEN_PRICE_UNAVAILABLE У токена нет курса, чтобы взять в нём комиссию
422 FEE_EXCEEDS_MAX_FEE Комиссия оказалась выше вашего maxFee
422 AMOUNT_DOES_NOT_COVER_FEE Задан subtractFeeFromAmount, а комиссия не меньше amount
422 RECIPIENT_ADDRESS_BLOCKED Адрес назначения не прошёл скрининг. Ничего не списано
422 DESTINATION_TAG_REQUIRED RIPPLE: счёт получателя не принимает платежи без тега назначения, а toTag не передан
422 DESTINATION_NOT_ACTIVATED RIPPLE: счёта получателя ещё нет, а выплата меньше суммы, нужной для его создания. details.minAmount — эта сумма, в drops
502 INTERNAL_ERROR Вывод не удалось оформить, либо неизвестно, оформлен ли он. Повторите с тем же externalId; если вы его не передавали, сначала проверьте свои выводы

201 — это ещё не конец

Принятый вывод всё ещё может провалиться по пути в сеть — вы увидите status: "FAILED" и причину в error. Считайте ответом терминальный статус, а не ответ на POST.

Отслеживание вывода

curl "https://api.pay.nullswap.com/api/v1/merchant/withdrawals?limit=50" \
  -H "x-api-key: sk_live_..."

Только offset, limit и order: фильтров по статусу, токену или externalId здесь нет. По умолчанию сортировка createdAt DESC.

{
  "id": "9c0d1e2f-3a4b-45c6-8d78-9e0f1a2b3c4d",
  "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
  "tokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
  "toAddress": "0xfedcba0987654321fedcba0987654321fedcba09",
  "toTag": null,
  "amount": "50000000",
  "fee": "1350000",
  "decimals": 6,
  "status": "CONFIRMED",
  "hash": "0x9a8b7c6d5e4f30211a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70819",
  "error": null,
  "createdAt": "2026-01-14T09:00:00.000Z",
  "updatedAt": "2026-01-14T09:04:00.000Z"
}

id — это тот самый transferRequestId, что вернулся вам при создании: то же значение под другим именем. amount — то, что отправлено получателю (уже за вычетом комиссии, если вы задали subtractFeeFromAmount), а fee — то, что с вас взяли сверх этого. toTag — тег назначения, с которым выплата ушла на RIPPLE; null, если тега не было, и на всех остальных сетях. toAddress всегда классический адрес, даже если вы отправили X-адрес. fee равен null у выплат, сделанных до того, как комиссию за вывод стали брать в выводимом токене.

status Значение
PENDING В очереди. В сети ничего нет, hash равен null
BROADCASTING Прямо сейчас подписывается и отправляется. hash обычно ещё null
BROADCAST Подписан и опубликован, hash заполнен. Подтверждения ещё нет
CONFIRMED Подтверждён на нужную сети глубину. Терминальный и окончательный
FAILED Не отправлен либо отправлен и откачен. Терминальный, причина — в error
stateDiagram-v2
    [*] --> PENDING: POST принят
    PENDING --> BROADCASTING: отправляется
    BROADCASTING --> BROADCAST: опубликован
    BROADCASTING --> FAILED: не удалось отправить
    PENDING --> FAILED: не удалось отправить
    BROADCAST --> CONFIRMED: подтверждён в сети
    BROADCAST --> FAILED: откачен
    CONFIRMED --> [*]
    FAILED --> [*]

Выводы не отправляют вебхуков

Выводов не касается ни одно событие из каталога. Опрашивайте этот эндпоинт, пока каждая интересующая вас запись не станет CONFIRMED или FAILED.

Две детали, которые нужно учесть в опрашивающем коде:

  • У записи FAILED hash может быть null — и навсегда. У вывода, провалившегося ещё до публикации, хеша не было никогда. Проверяйте поле, прежде чем показывать ссылку на обозреватель.
  • decimals тоже может быть null. Это значит, что токен с тех пор делистнули. Возьмите закешированное значение из /api/v1/tokens, но не подставляйте по умолчанию 18.

Вывод, провалившийся до попадания в сеть, возвращает на баланс и сумму, и fee. Вывод, который был отправлен и откачен, возвращает только сумму: сетевая комиссия уже потрачена.

Два случая автоматически не урегулируются, и снаружи ни один из них не выглядит иначе:

  • Вывод FAILED, который, возможно, дошёл до сети. Когда отправка обрывается по таймауту так, что неизвестно, ушла ли транзакция, запись получает FAILED, но ничего не возвращается: выплата ещё может дойти. По записи не понять, какой это FAILED: сверьте баланс и, если деньги не вернулись, обратитесь в поддержку с id вывода.
  • Вывод BROADCAST, который сеть отбросила. Он остаётся BROADCAST, и ничего не возвращается. Если вывод остаётся BROADCAST намного дольше времени подтверждения в этой сети, обратитесь в поддержку с его id и hash.

error заполняется только при FAILED, кодом: WITHDRAWAL_REVERTED — транзакция попала в сеть и откатилась, сумма вернулась на баланс, сетевая комиссия потрачена; WITHDRAWAL_FAILED — всё остальное, ничего не отправлено. Незнакомый код считайте WITHDRAWAL_FAILED: коды могут добавляться.