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.
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.
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
| Name | Type | Description |
|---|---|---|
| network_slug | string | Exact watch network key (trimmed, lowercased), e.g. ethereum. Max 255. (optional) |
| status | string | pending, check, stopped, confirmed, failed or cancelled. Anything else returns 400 watch.invalid_tx_status. (optional) |
| tx_hash | string | Case-insensitive prefix of the transaction hash. Max 255. (optional) |
| page | integer | Page number, 1–10000 (default 1). (optional) |
| per_page | integer | Page size, 1–100 (default 20). (optional) |
Response fields
| Name | Type | Description |
|---|---|---|
| items[].watch_id | string | Public watch identifier (uuid). Also sent as watch_id in the tx event. Use it to delete the watch. |
| items[].network_slug | string | Watch network key: the base slug on mainnet (ethereum), {network}-{env} elsewhere (ethereum-sepolia). |
| items[].tx_hash | string | Transaction hash, trimmed and lowercased. |
| items[].status | string | pending (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_height | number | Height of the block the transaction was found in. 0 until it is found. |
| items[].confirm_at_height | number | Chain 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_fee | string | Fee paid, in the network coin as a decimal string. Absent until the watch ends with a result. (optional) |
| items[].checked_height | number | Chain height at which the transaction was last checked. Absent before the first check. (optional) |
| items[].created_at | string | When the watch was created, ISO 8601 with offset. |
| slots | number | Plan cap for concurrently open tx watches. 0 means Tx Watch is not available on the plan. |
| slots_used | number | Open watches (pending or check) that hold a slot now. |
| pagination.page | number | Current page. |
| pagination.per_page | number | Page size used for this response. |
| pagination.total | number | Total number of matching watches. |
| pagination.has_more | boolean | True 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
| Name | Type | Description |
|---|---|---|
| network_slug | string | Watch 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_hash | string | Transaction hash to follow (max 128). It is trimmed and lowercased. |
Response fields
| Name | Type | Description |
|---|---|---|
| item | object | The created (or already open) watch: the same fields as a list item. |
| item.watch_id | string | Public watch identifier (uuid). Also sent as watch_id in the tx event. Use it to delete the watch. |
| item.network_slug | string | Watch network key: the base slug on mainnet (ethereum), {network}-{env} elsewhere (ethereum-sepolia). |
| item.tx_hash | string | Transaction hash, trimmed and lowercased. |
| item.status | string | pending (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_height | number | Height of the block the transaction was found in. 0 until it is found. |
| item.confirm_at_height | number | Chain 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_fee | string | Fee paid, in the network coin as a decimal string. Absent until the watch ends with a result. (optional) |
| item.checked_height | number | Chain height at which the transaction was last checked. Absent before the first check. (optional) |
| item.created_at | string | When 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.