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

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

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

Балансы

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

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

Одна строка — это одно сочетание (токен, кошелёк, kind), поэтому токен, на котором есть и чистые, и помеченные средства, встречается дважды, как в примере выше. Строка появляется, только когда есть что хранить: токена, в котором вам ни разу не платили, в ответе просто не будет — и нуля тоже не будет.

CLEAN и DIRTY

kind Значение
CLEAN Скрининг пройден либо не применялся. Вывести можно только это
DIRTY Восходит к платежу, не прошедшему AML-скрининг. Хранится отдельно

Никогда не складывайте эти два значения

Это не две половины одной доступной суммы: вывод берёт деньги только из CLEAN. Сложив их, вы получите число, которое перевести не сможете, — и узнаете об этом не раньше, чем вывод примут, а потом он провалится.

Превратить DIRTY в CLEAN через API нельзя — такого маршрута нет. Если помеченный баланс кажется вам ошибкой, напишите нам, приложив amlResultId того платежа, из-за которого он появился.

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

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

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 да Адрес назначения в нативном формате сети
amount string да Базовые единицы. "50000000" — это 50 USDT при 6 decimals
externalId string нет Ваша ссылка. Ключ идемпотентности — см. ниже
{
  "transferRequestId": "9c0d1e2f-3a4b-45c6-8d78-9e0f1a2b3c4d",
  "status": "PENDING",
  "hash": null
}

201 значит принято к обработке, а не отправлено. До сети дело ещё не дошло, hash равен null, результат придёт асинхронно.

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

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

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

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

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

Отправляйте его всегда

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

Ошибки

Статус message Причина
400 InvalidRequestBodyError Поля нет либо amount — не числовая строка
403 MerchantIsBlockedError Мерчант заблокирован
404 ChainWithIdNotFoundError Неизвестный chainId
404 TokenWithIdNotFoundError Неизвестный tokenId либо токен не из этой сети
404 MerchantWalletForChainTypeNotFoundError Для этого семейства сетей не настроен кошелёк
502 WalletServiceError Сервис кошельков оказался недоступен. Повторите с тем же externalId

Нехватку баланса не обязательно поймают в момент запроса

Не читайте 201 как «деньги были на месте». Вывод на сумму больше вашего баланса CLEAN вполне может быть принят и провалиться уже потом — вы увидите 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",
  "walletId": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c",
  "chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
  "tokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
  "toAddress": "0xfedcba0987654321fedcba0987654321fedcba09",
  "amount": "50000000",
  "decimals": 6,
  "status": "CONFIRMED",
  "hash": "0x9a8b7c6d5e4f30211a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f70819",
  "error": null,
  "createdAt": "2026-01-14T09:00:00.000Z",
  "updatedAt": "2026-01-14T09:04:00.000Z"
}

id — это тот самый transferRequestId, что вернулся вам при создании: то же значение под другим именем.

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

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

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

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

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

error — свободный текст, он заполняется только при FAILED и содержит что-нибудь вроде InsufficientGasError. Пишите его в лог и показывайте человеку, но не стройте на нём логику: это не стабильное перечисление.