Public API · Transaction watch

Transaction watch

A Tx watch follows one transaction hash on one network until it reaches the confirmations your account needs, fails, or turns out not to exist. You do not poll: when the watch ends, a tx event is delivered to your webhook or WebSocket (if enabled), and the watch stays readable here. This is not Address Watch: you add hashes you already know, and each watch ends once. All routes require signed headers (see Authentication) and use the base URL from the Overview. Success responses carry ok: true; failures carry ok: false, error_code and error.

transactionstx hashconfirmationstx-watchwebhook

← Public API

Flow diagram

A transaction watch is for a hash you already know. It follows the hash to a final result and ends once, with one tx event.

Your serverPublic APIRpcNodeBlockchain1. POST /v1/tx-watches {network_slug, tx_hash }2. { watch_id, status:pending }pending: the hash isnot seen in a blockyet. If the hash neverappears on thenetwork, the watchends as cancelled.3. The transaction isincluded in a block(start_height is set)check: waiting forconfirmations. Atconfirm_at_height theresult is decided:confirmed or failed.If a chainreorganization removesthe transaction fromits block, the watchgoes back to pending.4. New blocks on top: therequired confirmationsare reached5. tx event: confirmed, failed or cancelled (one perwatch; charged once)6. GET/v1/tx-watches?tx_hash=…(read the ended watch)
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 a watch ends and how you are told

A watch moves pending (hash not seen yet) to check (found in a block, waiting for confirmations) to confirmed or failed once the chain reaches confirm_at_height and the result is decided. A hash that never appears on the network ends as cancelled. If the transaction disappears from its block (a chain reorganization), the watch goes back to pending and the process starts over.

When the watch ends, one tx event is sent per ending: event confirmed, failed or cancelled. It is delivered through the same channels as address events: the webhook (GET/PUT /v1/webhooks, a URL is required and the tx type follows the payment/payout network lists) and, if you enabled it, the WebSocket as event tx on your channel. See the Webhooks and WebSockets articles for signing, retries and the common header (schema_version, delivery_id, type, event, network).

Each tx delivery costs the tx watch webhook credits of the network (see network credits). A transaction that failed on chain is as final as a successful one and is reported as event failed.

The body has the common header plus tx_hash and watch_id. For confirmed and failed it also carries the transaction as found in its block: block_height, block_hash, block_time, from_address, to_address, status, method, token, currency, amount, amount_raw, fee, fee_raw, confirmations, required_confirmations and confirmed. Unknown values are omitted, not sent as null. There is no address or direction: the watch is about a hash, not an address. For cancelled there is no block, so only the header, tx_hash and watch_id remain.

tx event (confirmed)

{
  "schema_version": "2",
  "delivery_id": "…",
  "type": "tx",
  "event": "confirmed",
  "network": "ethereum",
  "tx_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
  "block_height": 21540010,
  "block_hash": "0x…",
  "block_time": 1787393700,
  "watch_id": "7b1f5a52-52c0-4c4d-a1f0-0e5f2d6c9a31",
  "from_address": "0x…",
  "to_address": "0x…",
  "status": "success",
  "method": "transfer",
  "amount_raw": "30000000",
  "confirmations": 12,
  "required_confirmations": 12,
  "confirmed": true
}

Errors

Failed calls answer { ok: false, error_code, error } with an HTTP status. A request that cannot be accepted (wrong body, bad query value) is 400 with an errors list: every field that failed is named in it, and error_code is common.validation_failed when more than one field failed.

watch.tx_hash_required (400): network_slug or tx_hash is empty or too long. watch.scan_not_enabled (400): the network key cannot be watched. watch.no_plan (400): the account has no plan. watch.invalid_tx_status (400): the status filter is not one of the statuses above.

watch.tx_unavailable (403): the plan has no tx watch slots. watch.tx_limit (403): all slots are held by open watches; delete one or wait for it to end.

watch.ingest_unavailable (503): temporarily unavailable. On add, the watch is not created and holds no slot, so retry later; on delete, retry the delete.

Slots exhausted

{
  "ok": false,
  "error_code": "watch.tx_limit",
  "error": "Tx watch slots limit reached (10). Upgrade your plan to add more."
}

GET /v1/tx-watches

List your tx watches, newest first: the open ones and the ones that ended. Also returns the slots of the plan.

Query parameters

NameTypeDescription
network_slugstringExact watch network key (trimmed, lowercased), e.g. ethereum. Max 255. (optional)
statusstringpending, check, stopped, confirmed, failed or cancelled. Anything else returns 400 watch.invalid_tx_status. (optional)
tx_hashstringCase-insensitive prefix of the transaction hash. Max 255. (optional)
pageintegerPage number, 1–10000 (default 1). (optional)
per_pageintegerPage size, 1–100 (default 20). (optional)

Response fields

NameTypeDescription
items[].watch_idstringPublic watch identifier (uuid). Also sent as watch_id in the tx event. Use it to delete the watch.
items[].network_slugstringWatch network key: the base slug on mainnet (ethereum), {network}-{env} elsewhere (ethereum-sepolia).
items[].tx_hashstringTransaction hash, trimmed and lowercased.
items[].statusstringpending (not seen in a block yet), check (seen, waiting for confirmations), confirmed, failed (the transaction failed on chain) or cancelled (the hash never appeared on the network).
items[].start_heightnumberHeight of the block the transaction was found in. 0 until it is found.
items[].confirm_at_heightnumberChain height at which the result is decided (block height plus required confirmations). While the watch is still pending it holds the number of confirmations required instead.
items[].network_feestringFee paid, in the network coin as a decimal string. Absent until the watch ends with a result. (optional)
items[].checked_heightnumberChain height at which the transaction was last checked. Absent before the first check. (optional)
items[].created_atstringWhen the watch was created, ISO 8601 with offset.
slotsnumberPlan cap for concurrently open tx watches. 0 means Tx Watch is not available on the plan.
slots_usednumberOpen watches (pending or check) that hold a slot now.
pagination.pagenumberCurrent page.
pagination.per_pagenumberPage size used for this response.
pagination.totalnumberTotal number of matching watches.
pagination.has_morebooleanTrue if more pages exist after this one.
  • Sign the path together with the query string (see Authentication): the query is part of the signature.
  • A page, per_page or text value outside its bounds is rejected with 400 and an errors list naming the parameter; it is never replaced by a default.
  • Open watches (pending, check) have no time limit and each holds one slot. Ended ones (confirmed, failed, cancelled) hold no slot.
  • slots_used counts only open watches, so it is not the same as pagination.total.

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/tx-watches'))

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

Response example

{
  "ok": true,
  "items": [
    {
      "watch_id": "7b1f5a52-52c0-4c4d-a1f0-0e5f2d6c9a31",
      "network_slug": "ethereum",
      "tx_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
      "status": "check",
      "start_height": 21540010,
      "confirm_at_height": 21540022,
      "checked_height": 21540014,
      "created_at": "2026-08-22T10:15:00+00:00"
    },
    {
      "watch_id": "c9fd242d-525b-4b81-9a99-b2a95be331c9",
      "network_slug": "tron",
      "tx_hash": "bb08e86bca3219613b2d10f8b731c139aebc74c7a85f2a736b18bed50a2c320c",
      "status": "confirmed",
      "start_height": 85558636,
      "confirm_at_height": 85558655,
      "network_fee": "13.3",
      "checked_height": 85558655,
      "created_at": "2026-08-22T09:40:12+00:00"
    }
  ],
  "slots": 10,
  "slots_used": 1,
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total": 2,
    "has_more": false
  }
}

POST /v1/tx-watches

Start watching a transaction hash on a network. Takes one tx slot until the watch ends.

Request body

NameTypeDescription
network_slugstringWatch network key (max 80, trimmed, lowercased). Mainnet is the base network slug (ethereum); other environments are {network}-{env} (ethereum-sepolia). The environment must be available for watching, otherwise the request fails with watch.scan_not_enabled.
tx_hashstringTransaction hash to follow (max 128). It is trimmed and lowercased.

Response fields

NameTypeDescription
itemobjectThe created (or already open) watch: the same fields as a list item.
item.watch_idstringPublic watch identifier (uuid). Also sent as watch_id in the tx event. Use it to delete the watch.
item.network_slugstringWatch network key: the base slug on mainnet (ethereum), {network}-{env} elsewhere (ethereum-sepolia).
item.tx_hashstringTransaction hash, trimmed and lowercased.
item.statusstringpending (not seen in a block yet), check (seen, waiting for confirmations), confirmed, failed (the transaction failed on chain) or cancelled (the hash never appeared on the network).
item.start_heightnumberHeight of the block the transaction was found in. 0 until it is found.
item.confirm_at_heightnumberChain height at which the result is decided (block height plus required confirmations). While the watch is still pending it holds the number of confirmations required instead.
item.network_feestringFee paid, in the network coin as a decimal string. Absent until the watch ends with a result. (optional)
item.checked_heightnumberChain height at which the transaction was last checked. Absent before the first check. (optional)
item.created_atstringWhen the watch was created, ISO 8601 with offset.
  • Body fields are exact: a misspelled or unknown field is rejected with 400 and named in errors. Send Content-Type: application/json and sign the raw body.
  • The hash may be added before it reaches the network: the watch starts as pending. A hash that never appears on the network ends as cancelled.
  • A hash that is already being watched is not an error: the open watch is returned and no extra slot is taken. A hash whose earlier watch ended is watched again as a new watch (new watch_id) and replaces the old one in the list.
  • Required confirmations come from your account setting for that network, or the network default (at least 1). For a pending watch confirm_at_height holds that number; once the transaction is found it becomes the block height plus required confirmations.

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/tx-watches', '{"network_slug":"string","tx_hash":"string"}'))

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

Response example

{
  "ok": true,
  "item": {
    "watch_id": "7b1f5a52-52c0-4c4d-a1f0-0e5f2d6c9a31",
    "network_slug": "ethereum",
    "tx_hash": "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
    "status": "pending",
    "start_height": 0,
    "confirm_at_height": 12,
    "created_at": "2026-08-22T10:15:00+00:00"
  }
}

DELETE /v1/tx-watches/{watchId}

Stop watching a transaction and remove the watch. The watch id goes in the path; there is no request body.

  • watchId is the watch_id returned by add or list (a uuid).
  • Deleting is idempotent: an unknown, foreign, malformed or already removed id still returns ok: true. An ended watch can be deleted too; it disappears from the list.
  • A deleted open watch frees its slot and no tx event is sent for it.

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('DELETE', '/api/public/v1/tx-watches/00000000-0000-0000-0000-000000000000'))

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

Response example

{
  "ok": true
}

Tips

  • network_slug is the watch network key: the base catalog slug on mainnet (ethereum, tron) and {network}-{env} on other environments (ethereum-sepolia, tron-nile).
  • Add the hash right after you broadcast it; there is no need to wait until it appears on chain. Remember that every open watch holds a slot until it ends.
  • DELETE takes the watch id in the path (/v1/tx-watches/{watchId}) and no body.
  • Fields without a value are not written. Read an absent network_fee or checked_height as "not known yet".
  • Listing is a convenience, not the delivery channel: rely on the webhook or WebSocket event and use the list to reconcile.