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": "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" }
}

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

An invoice id that does not exist, or belongs to another merchant, is not an error here: the list comes back empty.

txHash is the hash of the on-chain transaction that carried the deposit — what you show your customer, or substitute for {value} in the chain's explorerTxUrlTemplate from GET /api/v1/chains. It can be null on older payments, so check it before you render a link. Do not confuse it with refundTxHash, the transaction you sent back.

Three independent statuses

A payment carries three status fields, and each answers a different question.

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 follow:

  • 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. EXPECTED and ALTERNATIVE payments start ACCEPTED — you asked for the first and opted in to the second with acceptsAnyToken, so there is nothing to decide. Screening changes that: a payment of any kind whose amlStatus comes back FAILED returns to PENDING, because flagged money is yours to decide about.

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. On RIPPLE an X-address is accepted and read as the address and tag it packs
refundTag no RIPPLE only: the destination tag to send the refund with, an integer 0–4294967295 or null. Only with refundAddress. Refused on any other chain, and when it differs from the tag of an X-address in refundAddress

A payment can be resolved when its resolution is PENDING and either its kind is not EXPECTED, or its amlStatus is FAILED. Expected money that passed screening settles on its own, so there is nothing to decide. Flagged expected money is the exception: it never counted towards the invoice, so the decision about it is yours.

Status message Cause
400 INVALID_PATH_PARAMETER invoiceId or paymentId is not a UUID
400 INVALID_REQUEST_BODY resolution is not one of its values, refundAddress is not a string, refundTag is not a whole number 0–4294967295, a refundTag on a chain other than RIPPLE, an X-address for another network, or one whose tag differs from refundTag
403 PERMISSION_DENIED The invoice belongs to another merchant
404 INVOICE_NOT_FOUND No such invoice
404 INVOICE_PAYMENT_NOT_FOUND No such payment, or it belongs to a different invoice
422 EXPECTED_INVOICE_PAYMENT_CANNOT_BE_RESOLVED EXPECTED payment that passed screening
422 INVOICE_PAYMENT_ALREADY_RESOLVED Already ACCEPTED or REFUNDED
422 REFUND_ADDRESS_REQUIRED REFUNDED without a refundAddress
422 REFUND_ADDRESS_NOT_ALLOWED refundAddress or refundTag sent with a non-REFUNDED resolution

The payment then carries what you named as refundAddress and refundTag — an X-address stored as its classic address and its tag. Refunding to an exchange account on RIPPLE almost always needs the customer's tag there: without it the money reaches the exchange and nobody's balance on it.

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
400 INVALID_PATH_PARAMETER invoiceId or paymentId is not a UUID
403 PERMISSION_DENIED The invoice belongs to another merchant
404 INVOICE_NOT_FOUND No such invoice
404 INVOICE_PAYMENT_NOT_FOUND No such payment, or it belongs to a different invoice
422 INVOICE_PAYMENT_NOT_REFUNDED The payment was not resolved as REFUNDED first
422 INVOICE_PAYMENT_REFUND_ALREADY_RECORDED 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..." }