Skip to content

Invoice payments

An invoice aggregates; a payment is one deposit. GET /api/v1/invoices/{invoiceId}/payments lists them individually — transaction, sender, amount, token, screening verdict and classification.

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": "PENDING",
    "status": "CONFIRMED",
    "confirmations": 19,
    "refundAddress": 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" }
}

Filters: id, kind, resolution, plus offset, limit, order and sortBy. This list defaults to order=ASC — oldest first — unlike every other list in the API.

Three fields that mean different things

A payment carries three orthogonal statuses. Reading one for another is the most common source of confusion here.

Field Answers Values
status Did the transaction stick? DETECTED, CONFIRMED, FAILED
amlStatus Did screening clear it? null, PASSED, FAILED, SKIPPED
kind Is it the money the invoice asked for? EXPECTED, ALTERNATIVE, UNEXPECTED

A payment must be CONFIRMED, cleared (PASSED or SKIPPED) and EXPECTED before it counts towards paidAmount. Any one of the three failing keeps the money out of the invoice total.

confirmations is a snapshot, not a live counter

It records the depth at the moment the record was written and is not incremented afterwards. Use status to decide whether a payment has settled. If you want to display depth, read latestBlockNumber and confirmationBlocks from GET /api/v1/chains.

How a payment is classified

kind is decided once, when the deposit is first seen, from the state of the invoice at that moment.

flowchart TD
    A["Deposit lands on the invoice's deposit address"] --> B{"Invoice still settling?<br/>WAITING · PAYMENT_DETECTED · PARTIALLY_PAID"}
    B -- no --> U["UNEXPECTED"]
    B -- yes --> C{"Same chain and same token<br/>as the invoice?"}
    C -- yes --> E["EXPECTED<br/>counts towards paidAmount"]
    C -- no --> D{"acceptsAnyToken"}
    D -- "true" --> F["ALTERNATIVE<br/>recorded, does not count"]
    D -- "false" --> U

When a payment is UNEXPECTED, unexpectedReason says why. A token or chain mismatch always wins over a timing reason:

unexpectedReason Meaning
WRONG_NETWORK_AND_TOKEN Neither the chain nor the token matches
WRONG_NETWORK Right token, wrong chain
WRONG_TOKEN Right chain, wrong token
AFTER_EXPIRATION Correct chain and token, but the invoice had already expired
AFTER_COMPLETION Correct chain and token, but the invoice had already moved past settling — PAID, SWAPPING, COMPLETED or FAILED

What counts towards the invoice

kind status Screening pendingAmount paidAmount
EXPECTED DETECTED not yet run +
EXPECTED CONFIRMED PASSED / SKIPPED +
EXPECTED CONFIRMED FAILED
EXPECTED FAILED any
ALTERNATIVE any any
UNEXPECTED any any

Two consequences worth stating plainly:

  • Flagged money can never pay an invoice. It is subtracted from pendingAmount and never added to paidAmount, whatever you later resolve it as.
  • Resolving a payment does not change any amount. ACCEPTED and REFUNDED record your decision for the audit trail; neither moves paidAmount.

Unexpected payments

resolution tracks your decision about a payment you were not asked for.

resolution Meaning
PENDING Waiting for you to decide
ACCEPTED You are keeping it
REFUNDED You are sending it back

UNEXPECTED payments start PENDING. ALTERNATIVE payments start ACCEPTED — you opted in to them with acceptsAnyToken, so there is nothing to decide, and they cannot be resolved.

Resolve a payment

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" }'
Field Required Notes
resolution yes ACCEPTED or REFUNDED
refundAddress conditional Required when REFUNDED, rejected otherwise

A payment can be resolved when it is PENDING and either its kind is not EXPECTED, or its amlStatus is FAILED. Clean expected money settles itself and is not yours to decide about; flagged expected money is the exception, because it never counted towards the invoice and somebody has to say where it goes.

Status message Cause
404 InvoicePaymentWithIdNotFoundError No such payment, or it belongs to a different invoice
422 ExpectedInvoicePaymentCannotBeResolvedError EXPECTED payment that passed screening
422 InvoicePaymentAlreadyResolvedError Already ACCEPTED or REFUNDED
422 RefundAddressRequiredError REFUNDED without a refundAddress
422 RefundAddressNotAllowedError refundAddress sent with a non-REFUNDED resolution

Record a refund

NullSwap does not send refunds

Resolving a payment as REFUNDED records a decision, not a transfer. You send the money back from your own wallet, then report the transaction so it lands on the record.

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" }'
Status message Cause
422 InvoicePaymentNotRefundedError The payment was not resolved as REFUNDED first
422 InvoicePaymentRefundAlreadyRecordedError A refundTxHash is already recorded

The full sequence for a payment you want to send back is therefore:

POST .../resolve   { "resolution": "REFUNDED", "refundAddress": "..." }
  -> you broadcast the refund from your own wallet
POST .../refund    { "refundTxHash": "0x..." }