Балансы и выводы¶
Деньги, свипнутые с инвойса или статического кошелька, попадают на ваш баланс. Вывод отправляет их оттуда на адрес, который укажете вы.
Балансы¶
{
"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 | нет | Ваша ссылка. Ключ идемпотентности — см. ниже |
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.
Две детали, которые стоит учесть в опрашивающем коде:
- У записи
FAILEDhashможет бытьnull— и навсегда. У вывода, провалившегося ещё до публикации, хеша не было никогда. Не рисуйте ссылку на обозреватель не глядя. decimalsтоже может бытьnull. Это значит, что токен с тех пор делистнули. Возьмите закешированное значение из/api/v1/tokens, но не подставляйте по умолчанию 18.
error — свободный текст, он заполняется только при FAILED и содержит что-нибудь вроде
InsufficientGasError. Пишите его в лог и показывайте человеку, но не стройте на нём логику: это не
стабильное перечисление.