Public API · Payments

Payments

A payment is a request for a fixed amount of a coin or token to an address, tied to your own order_id. You create it, show the address and amount to the customer, and we tell you at each step (money seen, confirmations, AML, paid, expired) by webhook or WebSocket. All routes require signed headers (see Authentication). Base URL: see Overview. Paths start with /v1/payments.

paymentsinvoiceorder_idUSDTAMLconfirmationswebhook

← Public API

Flow diagram

The path of one payment from the request to the paid event. Your server never polls: RpcNode pushes every status change to you.

Your serverPublic APIRpcNodeCustomer wallet1. POST /v1/payments {order_id, network_slug,address, amount }2. { payment_id, status:pending, expires_at }3. Show the address and the amount to pay4. Transfer on-chain tothe addressA transfer aftermin_block_height, ofthe same token and atleast the requestedamount, closes thepayment.5. payment_order, event=check (billed once)6. event=confirmations on every new block (free)7. event=aml_check when the confirmations are reached(AML networks)8. event=paid with the aml object: fulfil the orderOther endings and reconcilingNo transfer before expires_at: expired. You cancela pending payment: POST/v1/payments/{paymentId}/cancel gives cancelled.The transfer fails on-chain: failed. Each arrivesas a payment_order event.9. GET/v1/payments/orders/{orderId}after downtime or todouble-check
Solid arrows are requests, dashed arrows are answers, green arrows are what RpcNode pushes to you (webhook or WebSocket). Steps are numbered in time order.

How it works

1. POST /v1/payments with your order_id, the network, the receiving address, the amount and (for a token) the token contract. The payment starts as pending and has an expiry time.

2. The customer sends the funds to the address. A transfer in a later block than min_block_height, of the same token and of at least the requested amount, closes the payment. The payment becomes check and you get a payment_order event.

3. Each new block adds a confirmation. After required_confirmations the transfer is verified, then screened for AML risk where the network has an AML provider, and the payment becomes paid.

4. Match the paid notification to your order by order_id (or fetch the payment with GET /v1/payments/orders/{orderId}) and fulfil the order.

The receiving address is yours: you choose it per order (one address per payment is the cleanest way to tell customers apart).

Payments need a plan that includes them. The plan limits the number of open payments (payment slots), the number of payments created per calendar month (payment.not_available, payment.slot_limit and payment.monthly_limit, 403) and the number of addresses (watch.address_unavailable and watch.address_limit, 403).

Coin or token

A token is chosen only by its contract: send token_contract. Without token_contract the payment is in the coin of the network (TRX on Tron, ETH on Ethereum, ...). The coin is not a token and is not listed by GET /v1/network-tokens.

token_symbol is not accepted. A request that contains it is rejected with 400 network_token.symbol_not_accepted (or network_token.symbol_is_contract when the value looks like a contract: longer than 20 characters or starting with 0x), so a payment is never silently made in the coin when you meant a token.

The token must be listed for the network: take the contract from GET /v1/network-tokens. An unknown contract is rejected with 400 network_token.not_supported; a token that is switched off is rejected with network_token.disabled. Symbol and decimals are fixed by RpcNode, the request cannot set them.

Unknown fields are rejected too, not ignored: a typo such as token_contrakt returns 400 with the field named in errors.

Amount

Send amount as a string in whole units ("1.5", digits and one dot, no sign, no exponent) or amount_raw in the smallest units (decimal digits, or 0x and hex digits). Numbers are rejected: "amount": 1.5 returns 400 with "amount must be a string (write it in quotes)". If both are sent, amount_raw is used.

amount may have at most as many fractional digits as the token has decimals; 1.1234567 for a 6-decimal token is rejected with payment.too_many_fraction_digits. Texts longer than 100 characters are rejected (payment.amount_invalid, payment.amount_raw_invalid), the value must be greater than zero (payment.amount_not_positive, payment.amount_raw_not_positive) and fit in 77 digits in the smallest units (payment.amount_too_large).

Overpayment is accepted: a transfer of at least the requested amount closes the payment, and received_amount_raw shows what actually arrived. When two open payments on the same address and token fit one transfer, the exact amount wins, otherwise the newer payment does. Use a separate address per order, or different amounts, if you can have several open payments on one address.

Lifetime of a payment

Set the lifetime with ttl_seconds (1 to 2 592 000, that is up to 30 days) or with expires_at (ISO-8601 with an offset, e.g. 2026-10-20T12:00:00Z, must be in the future). If both are sent, ttl_seconds wins. If neither is sent the payment lives 3600 seconds (1 hour).

After expires_at an unpaid payment becomes expired within about 30 seconds and a transfer that arrives later no longer pays it (status expired, fail_reason expired). A transfer that was already seen in time keeps going through confirmations even if the expiry passes meanwhile.

Repeated requests and order_id

order_id (up to 128 characters, trimmed) is unique within your account. POST /v1/payments is idempotent per order_id: repeating the call with the same network, address, token and amount returns the existing payment with 200, in whatever state it is. An expired payment is not created again; make a new attempt with a new order_id.

Repeating an order_id with other parameters is refused with 409 payment.order_exists. Two simultaneous calls with the same order_id produce one payment.

GET /v1/payments/orders/{orderId} finds the payment by your own order number.

Payment lifecycle

A payment moves through these statuses. Every change is sent as a payment_order event (see Getting notified).

Transitions: pending to check, expired, cancelled; check to aml_check or paid or failed; aml_check to paid; underpaid to expired. paid, expired, failed and cancelled are final. pending, check, aml_check and underpaid occupy a payment slot, the final statuses release it.

If the AML check cannot be completed, aml.error says why and the payment stays in aml_check. Do not treat aml_check as paid; contact support for such a payment.

StatusMeaning
pendingCreated, waiting for a transfer. Counts as an open payment slot.
checkMoney was seen on-chain (tx_hash, block_height, received_amount_raw are set). The payment waits for confirmations; confirmations grows by one per block up to required_confirmations. Later transfers do not pay a payment that is in check.
aml_checkThe required confirmations were reached and the transfer was verified as successful; the risk screening is running. Only on networks that have an AML provider.
paidFinal success. On a network without AML this follows check directly; with AML it follows aml_check once the screening returned (the aml object is filled). Fulfil the order on paid.
underpaidA status for a payment that received less than requested. It stays open for the rest of the money until it expires. A transfer smaller than the requested amount does not pay the payment.
expiredNo money before expires_at (fail_reason expired), or the transfer was seen only after expiry.
failedThe transfer was seen but failed on the chain (fail_reason tx_failed).
cancelledYou cancelled a pending payment (fail_reason cancelled).

Happy path (events you receive)

pending                      POST /v1/payments
  | transfer seen          event=check            (billed once)
check
  | each new block         event=confirmations    (free, confirmations 1, 2, ...)
  | required reached       event=aml_check        (AML networks only)
aml_check
  | AML done               event=paid             (aml object filled)
paid

pending -> expired           event=expired     (no transfer before expires_at)
pending -> cancelled         event=cancelled   (POST .../cancel)
check   -> failed            event=failed      (the transfer failed on-chain)

Getting notified

Every status change is sent as a payment_order event, so you do not have to poll. Poll GET /v1/payments/{paymentId} only to reconcile (for example after downtime).

HTTP webhook: a signed POST to the webhook URL of your account, body type payment_order. Payments are sent to any enabled webhook with a URL; the Address Watch event filters do not apply. Set it up and learn the signature and retry rules in the Webhooks article (/public-api/webhooks). Any 2xx answer is a delivery; other answers and timeouts are retried with growing pauses (10 s, 30 s, 2 min, 10 min, 1 h, 6 h, seven attempts in total). Use delivery_id (also sent as X-RpcNode-Delivery) to ignore repeats.

WebSocket: event payment_order on the private-payments channel, same JSON as the webhook body. Payments arrive whenever your WebSocket is enabled. See the WebSockets article (/public-api/websockets). Events are not replayed while the socket is closed, reconcile with the API after reconnecting.

Credits: the payment event is billed once, on event=check (the first time the money is seen), at the webhook:payment price of the network. All other payment events (confirmations, aml_check, paid, expired, ...) are free. See Credits and usage (/errors/credits).

The event values are check, confirmations, aml_check, paid, underpaid, expired, failed and cancelled.

Webhook body versus the REST payment

The payment_order event carries the same data as the REST payment, with these differences: the network is in network (not network_slug); the token is an object token { contract, symbol, decimals } (null for the coin); the header fields schema_version ("2"), delivery_id, product ("payments"), type ("payment_order"), event and lifecycle (a copy of event) are added; and unknown values are written as explicit null instead of being left out.

For event underpaid, underpay_tx_hash and underpay_block are added: they name the transfer that came.

Fields of the payment_order body

Dates (expires_at, created_at, updated_at) are written as "2026-08-16T10:00:00+00:00". Fields without a value are null.

NameTypeDescription
schema_versionstringAlways "2".
delivery_idstringIdempotency key: the same delivery always has the same id (also sent in X-RpcNode-Delivery).
productstringAlways "payments".
typestringAlways "payment_order".
eventstringcheck, confirmations, aml_check, paid, underpaid, expired, failed or cancelled.
lifecyclestringA copy of event, for receivers of the old format.
payment_idstringPayment id (UUID).
order_idstringYour order number.
networkstringNetwork slug.
addressstringThe address the customer pays to.
amountstringRequested amount in whole units.
amount_rawstringRequested amount in the smallest units.
decimalsnumberDecimals of the coin or token.
tokenobject | null{ contract, symbol, decimals }; null for the native coin.
statusstringCurrent status of the payment.
tx_hashstring | nullThe paying transaction, once seen.
min_block_heightnumber | nullOnly transfers in blocks above this height can pay the payment.
block_heightnumber | nullThe block of the paying transaction.
confirmationsnumberConfirmations so far.
required_confirmationsnumberConfirmations needed for the payment to count.
final_blocknumber | nullThe block at which the required confirmations are reached.
received_amount_rawstring | nullWhat was received, in the smallest units.
network_feestring | nullFee of the paying transaction in whole units of network_fee_symbol.
network_fee_rawstring | nullThe same fee in the smallest units.
network_fee_symbolstring | nullSymbol of the fee coin.
fail_reasonstring | nullexpired, tx_failed or cancelled for a payment that did not succeed.
amlobject | nullThe screening result (see below); null until it is finished and on networks without AML.
expires_atstringWhen the payment expires.
created_atstringWhen the payment was created.
updated_atstringWhen the payment last changed.
underpay_tx_hashstringOnly for event underpaid: the transfer that came.
underpay_blocknumberOnly for event underpaid: its block.

payment_order, event=paid

{
  "schema_version": "2",
  "delivery_id": "…",
  "product": "payments",
  "type": "payment_order",
  "event": "paid",
  "payment_id": "5f0c1e0e-8c1b-4f6e-9a55-3d2d0f6a7b11",
  "order_id": "ord_123",
  "network": "tron",
  "address": "TToLVWCw5pxV241Rdt3StsUZRTb15QRzRG",
  "amount": "30",
  "amount_raw": "30000000",
  "decimals": 6,
  "token": { "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "symbol": "USDT", "decimals": 6 },
  "status": "paid",
  "tx_hash": "bb08e86bca3219613b2d10f8b731c139aebc74c7a85f2a736b18bed50a2c320c",
  "min_block_height": 85558636,
  "block_height": 85558640,
  "confirmations": 19,
  "required_confirmations": 19,
  "final_block": 85558658,
  "received_amount_raw": "30000000",
  "network_fee": "0.266",
  "network_fee_raw": "266000",
  "network_fee_symbol": "TRX",
  "fail_reason": null,
  "aml": {
    "score": 0,
    "risk_level": "low",
    "hacking_event": "",
    "detail_list": [],
    "risk_detail": [],
    "address_label": "",
    "checked_at": "2026-10-19T12:03:41+00:00"
  },
  "expires_at": "2026-10-19T13:00:00+00:00",
  "created_at": "2026-10-19T12:00:00+00:00",
  "updated_at": "2026-10-19T12:03:41+00:00",
  "lifecycle": "paid"
}

AML object in the webhook

The aml object of the webhook carries the provider report fields below. It has no provider name and no report link. It is null until the screening is finished, and null on networks without AML. The REST aml object is the normalized form of the same data (details, exposures).

NameTypeDescription
scorenumberRisk score.
risk_levelstringRisk level in lower case, for example low, moderate, high or severe.
hacking_eventstringName of a known hack the address is linked to; empty when none.
detail_listarrayRisk details by category.
risk_detailarrayExposure details.
address_labelstringLabel of the address when known; empty otherwise.
checked_atstringWhen the screening finished.

Responses and errors

Success: { ok: true, payment: {...} } for one payment, { ok: true, items: [...], pagination: {...} } for the list. Fields without a value are left out of REST responses, not written as null; read a missing field as "no value".

Errors: { ok: false, error_code, error }. A request body that cannot be accepted returns 400 with an errors list naming every wrong field: { field, error_code, error }. With one wrong field the top-level error_code is its code, with several it is common.validation_failed.

Payment codes: payment.network_required, payment.address_required, payment.order_id_required, payment.order_id_too_long (400; a network_slug longer than 80 or an address longer than 255 characters reports the matching *_required code), payment.amount_required, payment.amount_invalid, payment.amount_raw_invalid, payment.amount_too_large, payment.amount_not_positive, payment.amount_raw_not_positive, payment.too_many_fraction_digits, payment.ttl_invalid, payment.expires_invalid, payment.expires_in_past, payment.scan_not_enabled (400, payments are not available on this network), payment.invalid_status (400, list filter), payment.not_found (404), payment.order_exists (409), payment.not_pending (409, cancel), payment.not_available, payment.slot_limit, payment.monthly_limit (403, plan), payment.ingest_unavailable, payment.cancel_enqueue_failed (503, temporary: retry), payment.create_failed (500).

Token codes (400): network_token.not_supported (unknown contract on this network, or token_contract longer than 255 characters), network_token.disabled, network_token.symbol_not_accepted, network_token.symbol_is_contract, network_token.network_not_found (the network is unknown).

The address limits of your plan also apply on create: watch.address_unavailable, watch.address_limit (403) and watch.ingest_unavailable (503, temporary: retry). Authentication, signing and rate-limit errors are described in Authentication.

POST /v1/payments

Create a payment for an order: an address, an amount and a lifetime.

Request body

NameTypeDescription
order_idstringYour order number, 1 to 128 characters after trimming. Unique per account.
network_slugstringNetwork slug, up to 80 characters (lowercased). The network must be enabled for payments, otherwise payment.scan_not_enabled.
addressstringReceiving address, up to 255 characters (trimmed; 0x... addresses are lowercased). Not validated against the chain format.
amountstringAmount in whole units, e.g. "12.5" (digits and a dot, up to 100 characters, no more fractional digits than the token decimals). Required unless amount_raw is sent. (optional)
amount_rawstringAmount in the smallest units: decimal digits or 0x and hex digits, up to 100 characters. Takes priority over amount. Required unless amount is sent. (optional)
token_contractstringContract of the token, up to 255 characters, from GET /v1/network-tokens. Omit for the coin of the network. (optional)
ttl_secondsintegerLifetime in seconds, 1 to 2592000. Wins over expires_at. Default lifetime without either field: 3600. (optional)
expires_atstring (ISO-8601)Expiry time with an offset, e.g. 2026-10-20T12:00:00Z. Must be in the future. (optional)
token_symbolstringNot accepted. Sending it returns 400 network_token.symbol_not_accepted. Choose the token with token_contract. (optional)

Response fields

NameTypeDescription
okbooleantrue on success.
payment.payment_idstring (uuid)Id of the payment. Use it in GET /v1/payments/{paymentId} and in the cancel route.
payment.order_idstringYour own order number, exactly as you sent it (trimmed). Unique per account.
payment.network_slugstringNetwork slug, lowercase (tron, ethereum, tron-nile, ...).
payment.addressstringAddress the payer must send to (0x... addresses are lowercased, others keep their case).
payment.amountstringAmount in whole units of the asset ("12.5"), canonical form without trailing zeros.
payment.amount_rawstringThe same amount in the smallest units, a decimal integer string ("12500000").
payment.decimalsintegerDecimals of the asset (never taken from the request).
payment.token_contractstringContract of the token. Absent when the payment is in the coin of the network. (optional)
payment.token_symbolstringSymbol of the asset (USDT, TRX, ...). (optional)
payment.statusstringpending | check | aml_check | paid | underpaid | expired | failed | cancelled. See Payment lifecycle.
payment.tx_hashstringHash of the transfer that closed the payment. Absent until money is seen. (optional)
payment.min_block_heightintegerOnly transfers in blocks above this height can pay the payment (the chain height when it was created). (optional)
payment.block_heightintegerBlock of the transfer that closed the payment. Absent until money is seen. (optional)
payment.confirmationsintegerConfirmations counted so far (0 until money is seen, then up to required_confirmations).
payment.required_confirmationsintegerConfirmations needed before the payment moves on: your confirmation setting for the network, else the network default (at least 1).
payment.final_blockintegerBlock at which the transfer reaches required_confirmations. Absent until money is seen. (optional)
payment.received_amount_rawstringAmount of the transfer that arrived, in the smallest units. Absent until money is seen. (optional)
payment.network_feestringNetwork fee of the transfer in coin units. Present once the transfer is confirmed. (optional)
payment.network_fee_rawstringNetwork fee in the smallest units of the coin. Present together with network_fee. (optional)
payment.network_fee_symbolstringSymbol of the coin the fee is in (TRX, ETH, ...). Present together with network_fee. (optional)
payment.fail_reasonstringWhy the payment ended without being paid: expired, cancelled or tx_failed. Absent otherwise. (optional)
payment.expires_atstring (ISO-8601)When the payment stops accepting money (UTC). (optional)
payment.created_atstring (ISO-8601)Creation time (UTC).
payment.updated_atstring (ISO-8601)Time of the last change (UTC).
payment.amlobjectAML (risk) screening of the transfer. Absent while no check has happened or when the network has no AML provider. (optional)
payment.aml.scoreintegerRisk score, 0 to 100 (higher is riskier). Absent if the check produced no score. (optional)
payment.aml.risk_levelstringRisk level in lower case, for example low, moderate, high or severe. Absent if empty. (optional)
payment.aml.detailsstring[]Short risk notes from the provider. Always present inside aml (may be []).
payment.aml.exposuresobject[]Where the funds came from, highest percent first. Always present inside aml (may be []). Item: entity, risk_type, exposure_type (strings), hop_num (integer), volume and percent (numbers), hops ([{ hop, addresses[] }]).
payment.aml.hacking_eventstringName of a hack the funds are linked to, if the provider reports one. (optional)
payment.aml.address_labelstringLabel of the paying address, if the provider knows it. (optional)
payment.aml.report_urlstringLink to the full provider report, if there is one. (optional)
payment.aml.errorstringWhy the check could not be completed. Absent when it worked. (optional)
payment.aml.checked_atstring (ISO-8601)When the check finished. Absent while it is still running. (optional)
  • Idempotent per order_id: the same request returns the existing payment, other parameters return 409 payment.order_exists.
  • Unknown fields and fields of the wrong type are rejected with 400 and named in errors (for example "amount": 1.5 must be written "amount": "1.5").
  • On success the payment is pending and ready to receive money. On 503 payment.ingest_unavailable the payment was not created: repeat the same request.
  • Plan errors are 403: payment.not_available (the plan has no payments), payment.slot_limit, payment.monthly_limit. See the article text for the full list of error codes.

Request example

import { createHash, createHmac } from 'node:crypto'

const API_KEY = process.env.PUBLIC_API_KEY! // cabinet UUID
const SECRET = process.env.PUBLIC_API_SECRET! // sk_live_…
const BASE = 'https://api.rpcnode.dev'

// path: as the server receives it, with the query; body: the exact string that is sent
async function call(method: string, path: string, body = '') {
  const timestamp = Math.floor(Date.now() / 1000)
  const bodyHash = createHash('sha256').update(body).digest('hex')
  const signature = createHmac('sha256', SECRET)
    .update([timestamp, method, path, bodyHash].join('\n'))
    .digest('hex')
  const res = await fetch(BASE + path, {
    method,
    headers: {
      'X-Api-Key': API_KEY,
      'X-Timestamp': String(timestamp),
      'X-Signature': signature,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body || undefined,
  })
  return res.json()
}

console.log(await call('POST', '/api/public/v1/payments', '{"order_id":"string","network_slug":"string","address":"string"}'))

The request is signed in the snippet itself — see Authentication for the signing rules.

Response example

{
  "ok": true,
  "payment": {
    "payment_id": "5f0c1e0e-8c1b-4f6e-9a55-3d2d0f6a7b11",
    "order_id": "ord_123",
    "network_slug": "tron",
    "address": "TToLVWCw5pxV241Rdt3StsUZRTb15QRzRG",
    "amount": "30",
    "amount_raw": "30000000",
    "decimals": 6,
    "token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "token_symbol": "USDT",
    "status": "pending",
    "min_block_height": 85558636,
    "confirmations": 0,
    "required_confirmations": 19,
    "expires_at": "2026-10-19T13:00:00Z",
    "created_at": "2026-10-19T12:00:00Z",
    "updated_at": "2026-10-19T12:00:00Z"
  }
}

GET /v1/payments

List the payments of the account, newest first.

Query parameters

NameTypeDescription
statusstringOnly payments in this status (pending, check, aml_check, paid, underpaid, expired, failed, cancelled). (optional)
network_slugstringOnly payments on this network (lowercased). (optional)
addressstringOnly payments to this exact address (0x... addresses are lowercased before the comparison). (optional)
pageintegerPage number, 1 to 10000 (default 1). (optional)
per_pageintegerPage size, 1 to 100 (default 20). (optional)

Response fields

NameTypeDescription
okbooleantrue on success.
itemsobject[]Payments on this page (same object as GET /v1/payments/{paymentId}).
items[].payment_idstring (uuid)Id of the payment. Use it in GET /v1/payments/{paymentId} and in the cancel route.
items[].order_idstringYour own order number, exactly as you sent it (trimmed). Unique per account.
items[].network_slugstringNetwork slug, lowercase (tron, ethereum, tron-nile, ...).
items[].addressstringAddress the payer must send to (0x... addresses are lowercased, others keep their case).
items[].amountstringAmount in whole units of the asset ("12.5"), canonical form without trailing zeros.
items[].amount_rawstringThe same amount in the smallest units, a decimal integer string ("12500000").
items[].decimalsintegerDecimals of the asset (never taken from the request).
items[].token_contractstringContract of the token. Absent when the payment is in the coin of the network. (optional)
items[].token_symbolstringSymbol of the asset (USDT, TRX, ...). (optional)
items[].statusstringpending | check | aml_check | paid | underpaid | expired | failed | cancelled. See Payment lifecycle.
items[].tx_hashstringHash of the transfer that closed the payment. Absent until money is seen. (optional)
items[].min_block_heightintegerOnly transfers in blocks above this height can pay the payment (the chain height when it was created). (optional)
items[].block_heightintegerBlock of the transfer that closed the payment. Absent until money is seen. (optional)
items[].confirmationsintegerConfirmations counted so far (0 until money is seen, then up to required_confirmations).
items[].required_confirmationsintegerConfirmations needed before the payment moves on: your confirmation setting for the network, else the network default (at least 1).
items[].final_blockintegerBlock at which the transfer reaches required_confirmations. Absent until money is seen. (optional)
items[].received_amount_rawstringAmount of the transfer that arrived, in the smallest units. Absent until money is seen. (optional)
items[].network_feestringNetwork fee of the transfer in coin units. Present once the transfer is confirmed. (optional)
items[].network_fee_rawstringNetwork fee in the smallest units of the coin. Present together with network_fee. (optional)
items[].network_fee_symbolstringSymbol of the coin the fee is in (TRX, ETH, ...). Present together with network_fee. (optional)
items[].fail_reasonstringWhy the payment ended without being paid: expired, cancelled or tx_failed. Absent otherwise. (optional)
items[].expires_atstring (ISO-8601)When the payment stops accepting money (UTC). (optional)
items[].created_atstring (ISO-8601)Creation time (UTC).
items[].updated_atstring (ISO-8601)Time of the last change (UTC).
items[].amlobjectAML (risk) screening of the transfer. Absent while no check has happened or when the network has no AML provider. (optional)
items[].aml.scoreintegerRisk score, 0 to 100 (higher is riskier). Absent if the check produced no score. (optional)
items[].aml.risk_levelstringRisk level in lower case, for example low, moderate, high or severe. Absent if empty. (optional)
items[].aml.detailsstring[]Short risk notes from the provider. Always present inside aml (may be []).
items[].aml.exposuresobject[]Where the funds came from, highest percent first. Always present inside aml (may be []). Item: entity, risk_type, exposure_type (strings), hop_num (integer), volume and percent (numbers), hops ([{ hop, addresses[] }]).
items[].aml.hacking_eventstringName of a hack the funds are linked to, if the provider reports one. (optional)
items[].aml.address_labelstringLabel of the paying address, if the provider knows it. (optional)
items[].aml.report_urlstringLink to the full provider report, if there is one. (optional)
items[].aml.errorstringWhy the check could not be completed. Absent when it worked. (optional)
items[].aml.checked_atstring (ISO-8601)When the check finished. Absent while it is still running. (optional)
pagination.pageintegerCurrent page.
pagination.per_pageintegerPage size used.
pagination.totalintegerNumber of payments matching the filters.
pagination.has_morebooleantrue if there is a next page.
  • A wrong parameter value returns 400 and names the parameter; it is not replaced by a default.
  • An unknown status returns 400 payment.invalid_status.

Request example

import { createHash, createHmac } from 'node:crypto'

const API_KEY = process.env.PUBLIC_API_KEY! // cabinet UUID
const SECRET = process.env.PUBLIC_API_SECRET! // sk_live_…
const BASE = 'https://api.rpcnode.dev'

// path: as the server receives it, with the query; body: the exact string that is sent
async function call(method: string, path: string, body = '') {
  const timestamp = Math.floor(Date.now() / 1000)
  const bodyHash = createHash('sha256').update(body).digest('hex')
  const signature = createHmac('sha256', SECRET)
    .update([timestamp, method, path, bodyHash].join('\n'))
    .digest('hex')
  const res = await fetch(BASE + path, {
    method,
    headers: {
      'X-Api-Key': API_KEY,
      'X-Timestamp': String(timestamp),
      'X-Signature': signature,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body || undefined,
  })
  return res.json()
}

console.log(await call('GET', '/api/public/v1/payments'))

The request is signed in the snippet itself — see Authentication for the signing rules.

Response example

{
  "ok": true,
  "items": [
    {
      "payment_id": "5f0c1e0e-8c1b-4f6e-9a55-3d2d0f6a7b11",
      "order_id": "ord_123",
      "network_slug": "tron",
      "address": "TToLVWCw5pxV241Rdt3StsUZRTb15QRzRG",
      "amount": "30",
      "amount_raw": "30000000",
      "decimals": 6,
      "token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
      "token_symbol": "USDT",
      "status": "pending",
      "min_block_height": 85558636,
      "confirmations": 0,
      "required_confirmations": 19,
      "expires_at": "2026-10-19T13:00:00Z",
      "created_at": "2026-10-19T12:00:00Z",
      "updated_at": "2026-10-19T12:00:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 20, "total": 1, "has_more": false }
}

GET /v1/payments/{paymentId}

One payment by its payment_id.

Response fields

NameTypeDescription
okbooleantrue on success.
payment.payment_idstring (uuid)Id of the payment. Use it in GET /v1/payments/{paymentId} and in the cancel route.
payment.order_idstringYour own order number, exactly as you sent it (trimmed). Unique per account.
payment.network_slugstringNetwork slug, lowercase (tron, ethereum, tron-nile, ...).
payment.addressstringAddress the payer must send to (0x... addresses are lowercased, others keep their case).
payment.amountstringAmount in whole units of the asset ("12.5"), canonical form without trailing zeros.
payment.amount_rawstringThe same amount in the smallest units, a decimal integer string ("12500000").
payment.decimalsintegerDecimals of the asset (never taken from the request).
payment.token_contractstringContract of the token. Absent when the payment is in the coin of the network. (optional)
payment.token_symbolstringSymbol of the asset (USDT, TRX, ...). (optional)
payment.statusstringpending | check | aml_check | paid | underpaid | expired | failed | cancelled. See Payment lifecycle.
payment.tx_hashstringHash of the transfer that closed the payment. Absent until money is seen. (optional)
payment.min_block_heightintegerOnly transfers in blocks above this height can pay the payment (the chain height when it was created). (optional)
payment.block_heightintegerBlock of the transfer that closed the payment. Absent until money is seen. (optional)
payment.confirmationsintegerConfirmations counted so far (0 until money is seen, then up to required_confirmations).
payment.required_confirmationsintegerConfirmations needed before the payment moves on: your confirmation setting for the network, else the network default (at least 1).
payment.final_blockintegerBlock at which the transfer reaches required_confirmations. Absent until money is seen. (optional)
payment.received_amount_rawstringAmount of the transfer that arrived, in the smallest units. Absent until money is seen. (optional)
payment.network_feestringNetwork fee of the transfer in coin units. Present once the transfer is confirmed. (optional)
payment.network_fee_rawstringNetwork fee in the smallest units of the coin. Present together with network_fee. (optional)
payment.network_fee_symbolstringSymbol of the coin the fee is in (TRX, ETH, ...). Present together with network_fee. (optional)
payment.fail_reasonstringWhy the payment ended without being paid: expired, cancelled or tx_failed. Absent otherwise. (optional)
payment.expires_atstring (ISO-8601)When the payment stops accepting money (UTC). (optional)
payment.created_atstring (ISO-8601)Creation time (UTC).
payment.updated_atstring (ISO-8601)Time of the last change (UTC).
payment.amlobjectAML (risk) screening of the transfer. Absent while no check has happened or when the network has no AML provider. (optional)
payment.aml.scoreintegerRisk score, 0 to 100 (higher is riskier). Absent if the check produced no score. (optional)
payment.aml.risk_levelstringRisk level in lower case, for example low, moderate, high or severe. Absent if empty. (optional)
payment.aml.detailsstring[]Short risk notes from the provider. Always present inside aml (may be []).
payment.aml.exposuresobject[]Where the funds came from, highest percent first. Always present inside aml (may be []). Item: entity, risk_type, exposure_type (strings), hop_num (integer), volume and percent (numbers), hops ([{ hop, addresses[] }]).
payment.aml.hacking_eventstringName of a hack the funds are linked to, if the provider reports one. (optional)
payment.aml.address_labelstringLabel of the paying address, if the provider knows it. (optional)
payment.aml.report_urlstringLink to the full provider report, if there is one. (optional)
payment.aml.errorstringWhy the check could not be completed. Absent when it worked. (optional)
payment.aml.checked_atstring (ISO-8601)When the check finished. Absent while it is still running. (optional)
  • A payment_id that is not a uuid or does not belong to your account returns 404 payment.not_found.

Request example

import { createHash, createHmac } from 'node:crypto'

const API_KEY = process.env.PUBLIC_API_KEY! // cabinet UUID
const SECRET = process.env.PUBLIC_API_SECRET! // sk_live_…
const BASE = 'https://api.rpcnode.dev'

// path: as the server receives it, with the query; body: the exact string that is sent
async function call(method: string, path: string, body = '') {
  const timestamp = Math.floor(Date.now() / 1000)
  const bodyHash = createHash('sha256').update(body).digest('hex')
  const signature = createHmac('sha256', SECRET)
    .update([timestamp, method, path, bodyHash].join('\n'))
    .digest('hex')
  const res = await fetch(BASE + path, {
    method,
    headers: {
      'X-Api-Key': API_KEY,
      'X-Timestamp': String(timestamp),
      'X-Signature': signature,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body || undefined,
  })
  return res.json()
}

console.log(await call('GET', '/api/public/v1/payments/00000000-0000-0000-0000-000000000000'))

The request is signed in the snippet itself — see Authentication for the signing rules.

Response example

{
  "ok": true,
  "payment": {
    "payment_id": "5f0c1e0e-8c1b-4f6e-9a55-3d2d0f6a7b11",
    "order_id": "ord_123",
    "network_slug": "tron",
    "address": "TToLVWCw5pxV241Rdt3StsUZRTb15QRzRG",
    "amount": "30",
    "amount_raw": "30000000",
    "decimals": 6,
    "token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "token_symbol": "USDT",
    "status": "paid",
    "tx_hash": "bb08e86bca3219613b2d10f8b731c139aebc74c7a85f2a736b18bed50a2c320c",
    "min_block_height": 85558636,
    "block_height": 85558640,
    "confirmations": 19,
    "required_confirmations": 19,
    "final_block": 85558658,
    "received_amount_raw": "30000000",
    "network_fee": "0.266",
    "network_fee_raw": "266000",
    "network_fee_symbol": "TRX",
    "expires_at": "2026-10-19T13:00:00Z",
    "created_at": "2026-10-19T12:00:00Z",
    "updated_at": "2026-10-19T12:03:41Z",
    "aml": {
      "score": 0,
      "risk_level": "low",
      "details": [],
      "checked_at": "2026-10-19T12:03:41Z",
      "exposures": []
    }
  }
}

GET /v1/payments/orders/{orderId}

One payment by your own order_id.

Response fields

NameTypeDescription
okbooleantrue on success.
payment.payment_idstring (uuid)Id of the payment. Use it in GET /v1/payments/{paymentId} and in the cancel route.
payment.order_idstringYour own order number, exactly as you sent it (trimmed). Unique per account.
payment.network_slugstringNetwork slug, lowercase (tron, ethereum, tron-nile, ...).
payment.addressstringAddress the payer must send to (0x... addresses are lowercased, others keep their case).
payment.amountstringAmount in whole units of the asset ("12.5"), canonical form without trailing zeros.
payment.amount_rawstringThe same amount in the smallest units, a decimal integer string ("12500000").
payment.decimalsintegerDecimals of the asset (never taken from the request).
payment.token_contractstringContract of the token. Absent when the payment is in the coin of the network. (optional)
payment.token_symbolstringSymbol of the asset (USDT, TRX, ...). (optional)
payment.statusstringpending | check | aml_check | paid | underpaid | expired | failed | cancelled. See Payment lifecycle.
payment.tx_hashstringHash of the transfer that closed the payment. Absent until money is seen. (optional)
payment.min_block_heightintegerOnly transfers in blocks above this height can pay the payment (the chain height when it was created). (optional)
payment.block_heightintegerBlock of the transfer that closed the payment. Absent until money is seen. (optional)
payment.confirmationsintegerConfirmations counted so far (0 until money is seen, then up to required_confirmations).
payment.required_confirmationsintegerConfirmations needed before the payment moves on: your confirmation setting for the network, else the network default (at least 1).
payment.final_blockintegerBlock at which the transfer reaches required_confirmations. Absent until money is seen. (optional)
payment.received_amount_rawstringAmount of the transfer that arrived, in the smallest units. Absent until money is seen. (optional)
payment.network_feestringNetwork fee of the transfer in coin units. Present once the transfer is confirmed. (optional)
payment.network_fee_rawstringNetwork fee in the smallest units of the coin. Present together with network_fee. (optional)
payment.network_fee_symbolstringSymbol of the coin the fee is in (TRX, ETH, ...). Present together with network_fee. (optional)
payment.fail_reasonstringWhy the payment ended without being paid: expired, cancelled or tx_failed. Absent otherwise. (optional)
payment.expires_atstring (ISO-8601)When the payment stops accepting money (UTC). (optional)
payment.created_atstring (ISO-8601)Creation time (UTC).
payment.updated_atstring (ISO-8601)Time of the last change (UTC).
payment.amlobjectAML (risk) screening of the transfer. Absent while no check has happened or when the network has no AML provider. (optional)
payment.aml.scoreintegerRisk score, 0 to 100 (higher is riskier). Absent if the check produced no score. (optional)
payment.aml.risk_levelstringRisk level in lower case, for example low, moderate, high or severe. Absent if empty. (optional)
payment.aml.detailsstring[]Short risk notes from the provider. Always present inside aml (may be []).
payment.aml.exposuresobject[]Where the funds came from, highest percent first. Always present inside aml (may be []). Item: entity, risk_type, exposure_type (strings), hop_num (integer), volume and percent (numbers), hops ([{ hop, addresses[] }]).
payment.aml.hacking_eventstringName of a hack the funds are linked to, if the provider reports one. (optional)
payment.aml.address_labelstringLabel of the paying address, if the provider knows it. (optional)
payment.aml.report_urlstringLink to the full provider report, if there is one. (optional)
payment.aml.errorstringWhy the check could not be completed. Absent when it worked. (optional)
payment.aml.checked_atstring (ISO-8601)When the check finished. Absent while it is still running. (optional)
  • 404 payment.not_found when there is no payment for this order_id on your account.

Request example

import { createHash, createHmac } from 'node:crypto'

const API_KEY = process.env.PUBLIC_API_KEY! // cabinet UUID
const SECRET = process.env.PUBLIC_API_SECRET! // sk_live_…
const BASE = 'https://api.rpcnode.dev'

// path: as the server receives it, with the query; body: the exact string that is sent
async function call(method: string, path: string, body = '') {
  const timestamp = Math.floor(Date.now() / 1000)
  const bodyHash = createHash('sha256').update(body).digest('hex')
  const signature = createHmac('sha256', SECRET)
    .update([timestamp, method, path, bodyHash].join('\n'))
    .digest('hex')
  const res = await fetch(BASE + path, {
    method,
    headers: {
      'X-Api-Key': API_KEY,
      'X-Timestamp': String(timestamp),
      'X-Signature': signature,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body || undefined,
  })
  return res.json()
}

console.log(await call('GET', '/api/public/v1/payments/orders/00000000-0000-0000-0000-000000000000'))

The request is signed in the snippet itself — see Authentication for the signing rules.

Response example

{
  "ok": true,
  "payment": {
    "payment_id": "5f0c1e0e-8c1b-4f6e-9a55-3d2d0f6a7b11",
    "order_id": "ord_123",
    "network_slug": "tron",
    "address": "TToLVWCw5pxV241Rdt3StsUZRTb15QRzRG",
    "amount": "30",
    "amount_raw": "30000000",
    "decimals": 6,
    "token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "token_symbol": "USDT",
    "status": "paid",
    "tx_hash": "bb08e86bca3219613b2d10f8b731c139aebc74c7a85f2a736b18bed50a2c320c",
    "min_block_height": 85558636,
    "block_height": 85558640,
    "confirmations": 19,
    "required_confirmations": 19,
    "final_block": 85558658,
    "received_amount_raw": "30000000",
    "network_fee": "0.266",
    "network_fee_raw": "266000",
    "network_fee_symbol": "TRX",
    "expires_at": "2026-10-19T13:00:00Z",
    "created_at": "2026-10-19T12:00:00Z",
    "updated_at": "2026-10-19T12:03:41Z",
    "aml": {
      "score": 0,
      "risk_level": "low",
      "details": [],
      "checked_at": "2026-10-19T12:03:41Z",
      "exposures": []
    }
  }
}

POST /v1/payments/{paymentId}/cancel

Cancel a payment that is still pending. Takes no body.

Response fields

NameTypeDescription
okbooleantrue on success.
payment.payment_idstring (uuid)Id of the payment. Use it in GET /v1/payments/{paymentId} and in the cancel route.
payment.order_idstringYour own order number, exactly as you sent it (trimmed). Unique per account.
payment.network_slugstringNetwork slug, lowercase (tron, ethereum, tron-nile, ...).
payment.addressstringAddress the payer must send to (0x... addresses are lowercased, others keep their case).
payment.amountstringAmount in whole units of the asset ("12.5"), canonical form without trailing zeros.
payment.amount_rawstringThe same amount in the smallest units, a decimal integer string ("12500000").
payment.decimalsintegerDecimals of the asset (never taken from the request).
payment.token_contractstringContract of the token. Absent when the payment is in the coin of the network. (optional)
payment.token_symbolstringSymbol of the asset (USDT, TRX, ...). (optional)
payment.statusstringpending | check | aml_check | paid | underpaid | expired | failed | cancelled. See Payment lifecycle.
payment.tx_hashstringHash of the transfer that closed the payment. Absent until money is seen. (optional)
payment.min_block_heightintegerOnly transfers in blocks above this height can pay the payment (the chain height when it was created). (optional)
payment.block_heightintegerBlock of the transfer that closed the payment. Absent until money is seen. (optional)
payment.confirmationsintegerConfirmations counted so far (0 until money is seen, then up to required_confirmations).
payment.required_confirmationsintegerConfirmations needed before the payment moves on: your confirmation setting for the network, else the network default (at least 1).
payment.final_blockintegerBlock at which the transfer reaches required_confirmations. Absent until money is seen. (optional)
payment.received_amount_rawstringAmount of the transfer that arrived, in the smallest units. Absent until money is seen. (optional)
payment.network_feestringNetwork fee of the transfer in coin units. Present once the transfer is confirmed. (optional)
payment.network_fee_rawstringNetwork fee in the smallest units of the coin. Present together with network_fee. (optional)
payment.network_fee_symbolstringSymbol of the coin the fee is in (TRX, ETH, ...). Present together with network_fee. (optional)
payment.fail_reasonstringWhy the payment ended without being paid: expired, cancelled or tx_failed. Absent otherwise. (optional)
payment.expires_atstring (ISO-8601)When the payment stops accepting money (UTC). (optional)
payment.created_atstring (ISO-8601)Creation time (UTC).
payment.updated_atstring (ISO-8601)Time of the last change (UTC).
payment.amlobjectAML (risk) screening of the transfer. Absent while no check has happened or when the network has no AML provider. (optional)
payment.aml.scoreintegerRisk score, 0 to 100 (higher is riskier). Absent if the check produced no score. (optional)
payment.aml.risk_levelstringRisk level in lower case, for example low, moderate, high or severe. Absent if empty. (optional)
payment.aml.detailsstring[]Short risk notes from the provider. Always present inside aml (may be []).
payment.aml.exposuresobject[]Where the funds came from, highest percent first. Always present inside aml (may be []). Item: entity, risk_type, exposure_type (strings), hop_num (integer), volume and percent (numbers), hops ([{ hop, addresses[] }]).
payment.aml.hacking_eventstringName of a hack the funds are linked to, if the provider reports one. (optional)
payment.aml.address_labelstringLabel of the paying address, if the provider knows it. (optional)
payment.aml.report_urlstringLink to the full provider report, if there is one. (optional)
payment.aml.errorstringWhy the check could not be completed. Absent when it worked. (optional)
payment.aml.checked_atstring (ISO-8601)When the check finished. Absent while it is still running. (optional)
  • Only a pending payment can be cancelled; any other status returns 409 payment.not_pending (a payment that already saw money cannot be cancelled).
  • Cancelling is asynchronous: the response shows the payment as it is now, so it may still read pending for a moment. The cancelled event follows by webhook or WebSocket, or read the payment again.
  • After cancel, transfers no longer pay the payment and its payment slot is released.
  • 503 payment.cancel_enqueue_failed: the cancellation was not accepted; try again.

Request example

import { createHash, createHmac } from 'node:crypto'

const API_KEY = process.env.PUBLIC_API_KEY! // cabinet UUID
const SECRET = process.env.PUBLIC_API_SECRET! // sk_live_…
const BASE = 'https://api.rpcnode.dev'

// path: as the server receives it, with the query; body: the exact string that is sent
async function call(method: string, path: string, body = '') {
  const timestamp = Math.floor(Date.now() / 1000)
  const bodyHash = createHash('sha256').update(body).digest('hex')
  const signature = createHmac('sha256', SECRET)
    .update([timestamp, method, path, bodyHash].join('\n'))
    .digest('hex')
  const res = await fetch(BASE + path, {
    method,
    headers: {
      'X-Api-Key': API_KEY,
      'X-Timestamp': String(timestamp),
      'X-Signature': signature,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
    },
    body: body || undefined,
  })
  return res.json()
}

console.log(await call('POST', '/api/public/v1/payments/00000000-0000-0000-0000-000000000000/cancel'))

The request is signed in the snippet itself — see Authentication for the signing rules.

Response example

{
  "ok": true,
  "payment": {
    "payment_id": "5f0c1e0e-8c1b-4f6e-9a55-3d2d0f6a7b11",
    "order_id": "ord_123",
    "network_slug": "tron",
    "address": "TToLVWCw5pxV241Rdt3StsUZRTb15QRzRG",
    "amount": "30",
    "amount_raw": "30000000",
    "decimals": 6,
    "token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "token_symbol": "USDT",
    "status": "pending",
    "min_block_height": 85558636,
    "confirmations": 0,
    "required_confirmations": 19,
    "expires_at": "2026-10-19T13:00:00Z",
    "created_at": "2026-10-19T12:00:00Z",
    "updated_at": "2026-10-19T12:00:00Z"
  }
}

Node.js: create a payment, verify the paid webhook, match by order_id

JavaScript

import crypto from 'node:crypto'
import express from 'express'

// publicApi(method, path, bodyObject) is the signing helper from the Authentication article.
const WEBHOOK_SECRET = process.env.RPCNODE_WEBHOOK_SECRET // the secret of your webhook (see Webhooks)
const TRC20_USDT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'

// 1. Create the payment when the customer checks out. Safe to repeat: same order_id and same
//    parameters return the same payment.
export async function createInvoice(order) {
  const res = await publicApi('POST', '/api/public/v1/payments', {
    order_id: order.id, // your order number, up to 128 characters
    network_slug: 'tron',
    address: order.depositAddress, // where the customer must pay
    amount: order.total, // a STRING, e.g. '30' or '12.5'
    token_contract: TRC20_USDT, // omit for the coin of the network
    ttl_seconds: 1800,
  })
  if (!res.ok) throw new Error(res.error_code + ': ' + res.error)
  await saveOrderPayment(order.id, res.payment.payment_id) // keep payment_id next to the order
  return res.payment // show address, amount and expires_at to the customer
}

// 2. Receive the webhook. Verify the signature over the RAW body, then match by order_id.
const app = express()

app.post('/rpcnode/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8')

  // X-RpcNode-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
  const parts = Object.fromEntries(
    String(req.get('X-RpcNode-Signature') || '')
      .split(',')
      .map((p) => p.split('=')),
  )
  const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(parts.t + '.' + raw).digest('hex')
  const given = Buffer.from(String(parts.v1 || ''), 'hex')
  const wanted = Buffer.from(expected, 'hex')
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300 // reject old replays
  if (!fresh || given.length !== wanted.length || !crypto.timingSafeEqual(given, wanted)) {
    return res.sendStatus(401)
  }

  const event = JSON.parse(raw)
  if (event.type !== 'payment_order') return res.sendStatus(200) // other products share this URL

  // Same event, same delivery_id: process each delivery once.
  if (await alreadyHandled(event.delivery_id)) return res.sendStatus(200)

  const order = await findOrder(event.order_id) // match by YOUR order_id
  if (order && order.paymentId === event.payment_id) {
    if (event.event === 'paid') {
      await markOrderPaid(order.id, event.tx_hash, event.received_amount_raw)
    } else if (event.event === 'expired' || event.event === 'cancelled' || event.event === 'failed') {
      await markOrderClosed(order.id, event.event, event.fail_reason)
    }
    // check, confirmations, aml_check and underpaid are progress: update the UI, do not ship yet.
  }

  await rememberHandled(event.delivery_id)
  res.sendStatus(200) // any 2xx is a delivery; anything else is retried
})

app.listen(3000)

Tips

  • Always send amount as a string and keep money out of floating point: compare amounts with amount_raw (BigInt in JavaScript).
  • Fulfil on event=paid (status paid) only. check and aml_check mean the money is seen but not final.
  • Identify the order by order_id and double-check payment_id and the token and amount before shipping.
  • Make the webhook handler idempotent by delivery_id and answer 2xx quickly; do the slow work afterwards.
  • Give every attempt a new order_id. An expired or cancelled order_id keeps its old payment.
  • Take token contracts from GET /v1/network-tokens; for the coin of the network leave token_contract out.
  • Use a separate receiving address per order, or distinct amounts, so one transfer can never fit two payments.