Платежи по инвойсу¶
Инвойс показывает итог, платёж — отдельный депозит. GET /api/v1/invoices/{invoiceId}/payments
перечисляет их по одному: транзакция, отправитель, сумма, токен, вердикт скрининга и классификация.
curl "https://api.pay.nullswap.com/api/v1/invoices/b2f4a6c8-.../payments?limit=50" \
-H "x-api-key: sk_live_..."
{
"data": [{
"id": "d4e5f6a7-b8c9-4012-a345-6b7c8d9e0f12",
"invoiceId": "b2f4a6c8-0d1e-4f23-9a45-6b7c8d9e0f11",
"chainTransactionId": "aa11bb22-cc33-4d44-8e55-6f7788990011",
"chainId": "1c9a7f36-5f2b-4a83-9d51-0e6b7c8d9a10",
"tokenId": "3b7e1a48-2c6d-4e9f-8a01-5d4c3b2a1908",
"fromAddress": "TJ8k...2Ba7",
"amount": "25500000",
"kind": "EXPECTED",
"unexpectedReason": null,
"resolution": "ACCEPTED",
"status": "CONFIRMED",
"confirmations": 19,
"txHash": "4f1e0c5d8b2a7e9f3c6d1b8a5e2f9c4d7a0b3e6f1c8d5a2b9e4f7c0d3a6b1e8f",
"refundAddress": null,
"refundTag": null,
"refundTxHash": null,
"amlResultId": "5c6d7e8f-9a0b-41c2-8d34-5e6f7a8b9c0d",
"amlStatus": "PASSED",
"createdAt": "2026-01-01T12:04:11.000Z",
"updatedAt": "2026-01-01T12:07:52.000Z"
}],
"pagination": { "offset": 0, "limit": 50, "total": 1, "order": "ASC" }
}
Фильтры: id, kind, resolution, плюс offset, limit, order и sortBy (CREATED_AT,
ID). В отличие от всех остальных списков в API, здесь по умолчанию order=ASC — сначала самые
старые.
Несуществующий id инвойса или id инвойса другого мерчанта здесь не ошибка: список просто приходит пустым.
txHash — хэш ончейн-транзакции, которой пришёл депозит. Его можно показать клиенту или подставить
вместо {value} в explorerTxUrlTemplate сети из GET /api/v1/chains.
У старых платежей он может быть null, поэтому проверяйте его, прежде чем строить ссылку. Не путайте
его с refundTxHash — транзакцией, которой вы вернули деньги.
Три независимых статуса¶
У платежа три статусных поля, и каждое отвечает на свой вопрос.
| Поле | Отвечает на вопрос | Значения |
|---|---|---|
status |
Закрепилась ли транзакция? | DETECTED, CONFIRMED, FAILED |
amlStatus |
Пропустил ли её скрининг? | null, PASSED, FAILED, SKIPPED |
kind |
Это те деньги, которые запрашивал инвойс? | EXPECTED, ALTERNATIVE, UNEXPECTED |
Чтобы деньги попали в paidAmount, платёж должен быть CONFIRMED, пройти скрининг (PASSED или
SKIPPED) и иметь kind = EXPECTED. Не выполнено хотя бы одно условие из трёх — в итог
инвойса деньги не попадут.
confirmations — это снимок, а не живой счётчик
Поле фиксирует глубину на момент записи и дальше не растёт. О завершённости платежа судите по
status. А если нужно показывать глубину, берите latestBlockNumber и confirmationBlocks из
GET /api/v1/chains.
Как классифицируется платёж¶
kind присваивается один раз — когда депозит замечен впервые, — и зависит от того, в каком состоянии
инвойс был в этот момент.
flowchart TD
A["Депозит приходит на депозитный адрес инвойса"] --> B{"Инвойс ещё в процессе оплаты?<br/>WAITING · PAYMENT_DETECTED · PARTIALLY_PAID"}
B -- нет --> U["UNEXPECTED"]
B -- да --> C{"Та же сеть и тот же токен,<br/>что и у инвойса?"}
C -- да --> E["EXPECTED<br/>засчитывается в paidAmount"]
C -- нет --> D{"acceptsAnyToken"}
D -- "true" --> F["ALTERNATIVE<br/>записывается, не засчитывается"]
D -- "false" --> U
Если платёж оказался UNEXPECTED, причину объясняет unexpectedReason. Несовпадение сети или токена
всегда перевешивает причину, связанную со временем:
unexpectedReason |
Значение |
|---|---|
WRONG_NETWORK_AND_TOKEN |
Не совпадают ни сеть, ни токен |
WRONG_NETWORK |
Токен верный, сеть неверная |
WRONG_TOKEN |
Сеть верная, токен неверный |
AFTER_EXPIRATION |
Сеть и токен верные, но срок инвойса уже истёк |
AFTER_COMPLETION |
Сеть и токен верные, но инвойс уже вышел из стадии оплаты — PAID, SWAPPING, COMPLETED или FAILED |
Что засчитывается в инвойс¶
kind |
status |
Скрининг | pendingAmount |
paidAmount |
|---|---|---|---|---|
EXPECTED |
DETECTED |
ещё не выполнен | + | — |
EXPECTED |
CONFIRMED |
PASSED / SKIPPED |
− | + |
EXPECTED |
CONFIRMED |
FAILED |
− | — |
EXPECTED |
FAILED |
любой | − | − |
ALTERNATIVE |
любой | любой | — | — |
UNEXPECTED |
любой | любой | — | — |
Отсюда два следствия:
- Помеченные деньги инвойс не оплатят никогда. Они вычитаются из
pendingAmountи вpaidAmountне попадают — чем бы вы потом ни закрыли такой платёж. - Разрешение платежа не двигает суммы.
ACCEPTEDиREFUNDEDфиксируют ваше решение для журнала аудита, и ни то, ни другое наpaidAmountне влияет.
Неожиданные платежи¶
resolution хранит ваше решение по деньгам, которых вы не просили.
resolution |
Значение |
|---|---|
PENDING |
Ждёт вашего решения |
ACCEPTED |
Вы оставляете его себе |
REFUNDED |
Вы отправляете его обратно |
Платёж UNEXPECTED появляется со статусом PENDING, платежи EXPECTED и ALTERNATIVE — сразу с
ACCEPTED: первые вы сами запросили, а принимать вторые согласились флагом acceptsAnyToken, так
что решать нечего. Скрининг это меняет: платёж любого kind, у которого amlStatus оказался
FAILED, возвращается в PENDING, потому что решение по помеченным деньгам — за вами.
Разрешение платежа¶
curl -X POST https://api.pay.nullswap.com/api/v1/invoices/{invoiceId}/payments/{paymentId}/resolve \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "resolution": "REFUNDED", "refundAddress": "TJ8k...2Ba7" }'
| Поле | Обяз. | Примечания |
|---|---|---|
resolution |
да | ACCEPTED или REFUNDED |
refundAddress |
условно | Обязателен при REFUNDED, в остальных случаях отклоняется. На RIPPLE принимается и X-адрес — он читается как адрес и тег, которые в нём упакованы |
refundTag |
нет | Только RIPPLE: тег назначения, с которым отправляется возврат, — целое число 0–4294967295 или null. Только вместе с refundAddress. Отклоняется на любой другой сети, а также если расходится с тегом X-адреса в refundAddress |
Разрешить платёж можно, пока его resolution равен PENDING, и при условии, что либо его
kind отличается от EXPECTED, либо amlStatus равен FAILED. Ожидаемые деньги, прошедшие
скрининг, расходятся сами — решать по ним нечего. Помеченные ожидаемые деньги — исключение: в
инвойс они так и не попали, поэтому решение по ним остаётся за вами.
| Статус | message |
Причина |
|---|---|---|
400 |
INVALID_PATH_PARAMETER |
invoiceId или paymentId — не UUID |
400 |
INVALID_REQUEST_BODY |
resolution не из списка значений, refundAddress — не строка, refundTag — не целое число 0–4294967295, refundTag на сети, отличной от RIPPLE, X-адрес другой сети либо X-адрес, чей тег расходится с refundTag |
403 |
PERMISSION_DENIED |
Инвойс принадлежит другому мерчанту |
404 |
INVOICE_NOT_FOUND |
Такого инвойса нет |
404 |
INVOICE_PAYMENT_NOT_FOUND |
Такого платежа нет, либо он принадлежит другому инвойсу |
422 |
EXPECTED_INVOICE_PAYMENT_CANNOT_BE_RESOLVED |
Платёж EXPECTED, прошедший скрининг |
422 |
INVOICE_PAYMENT_ALREADY_RESOLVED |
Уже ACCEPTED или REFUNDED |
422 |
REFUND_ADDRESS_REQUIRED |
REFUNDED без refundAddress |
422 |
REFUND_ADDRESS_NOT_ALLOWED |
refundAddress или refundTag отправлен с разрешением, отличным от REFUNDED |
После этого у платежа записаны названные вами refundAddress и refundTag — X-адрес сохраняется как
классический адрес и его тег. Возврат на биржевой счёт на RIPPLE почти всегда требует тега
покупателя на этой бирже: без него деньги дойдут до биржи, но ни на чей баланс там не попадут.
Регистрация возврата¶
NullSwap не отправляет возвраты
Разрешение REFUNDED фиксирует ваше решение, а не перевод. Деньги вы отправляете обратно сами,
со своего кошелька, а потом сообщаете нам хеш транзакции — чтобы он остался в истории платежа.
curl -X POST https://api.pay.nullswap.com/api/v1/invoices/{invoiceId}/payments/{paymentId}/refund \
-H "x-api-key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{ "refundTxHash": "0xabcdef...7890" }'
| Статус | message |
Причина |
|---|---|---|
400 |
INVALID_PATH_PARAMETER |
invoiceId или paymentId — не UUID |
403 |
PERMISSION_DENIED |
Инвойс принадлежит другому мерчанту |
404 |
INVOICE_NOT_FOUND |
Такого инвойса нет |
404 |
INVOICE_PAYMENT_NOT_FOUND |
Такого платежа нет, либо он принадлежит другому инвойсу |
422 |
INVOICE_PAYMENT_NOT_REFUNDED |
Платёж не был предварительно разрешён как REFUNDED |
422 |
INVOICE_PAYMENT_REFUND_ALREADY_RECORDED |
refundTxHash уже записан |
Целиком последовательность для платежа, который вы решили вернуть, выглядит так: