# Rarebl API Documentation > The Rarebl Open API (v1 and v2) gives blockchain data and tools for Bitcoin, Litecoin, Bitcoin Cash, Dash, Ethereum and TRON from Rarebl's own nodes: 42 HTTP endpoints at https://api.rarebl.com/open// (reads are GET, v2 writes are POST with a JSON body), a WebSocket stream, webhooks, market prices and signing SDKs. No API keys. ## Basics - **Base URL:** `https://api.rarebl.com/open` - **Chains (`chain` parameter):** btc, ltc, bch, dash, eth, tron - **Authentication:** none; `X-API-KEY` is reserved and ignored - **Rate limits (per IP, by class):** read 5/s burst 20; compute 2/s burst 10; write 6/min burst 3 and 300/day; watch 30/min; hooks 10/min; stream 3 connections; HTTP 429 with Retry-After - **Sending:** sign locally with an SDK (recommended), sign the digests yourself, or hosted signing (sends a key to Rarebl; needs the header X-Rarebl-Accept-Risk: hosted-signing) - **Envelope:** `{status, code, message, data, go, meta:{requestid, notices}}` (quote `meta.requestid`, or the `X-Request-Id` header, to support); codes API200, API400, API403, API404, API405, API409, API422, API429, API502, API500 - **Postman:** https://docs.rarebl.com/collections/rarebl-api.postman_collection.json This file combines the Rarebl guides and the full API reference for LLM ingestion. # Human documentation # Documentation The Rarebl Open API gives you blockchain data and tools for Bitcoin, Litecoin, Bitcoin Cash, Dash, Ethereum and TRON, served from Rarebl's own nodes: read data, build and broadcast transactions, webhooks, a live stream and market prices. Use the guides when you need context, then switch to API Reference for exact request and response details. The API is available at `https://api.rarebl.com`, with endpoints under `/open//`. See [Coverage and limits](https://docs.rarebl.com/limits-today) for what is available on each chain. ## Jump right in - [Quickstart](https://docs.rarebl.com/quickstart): Make your first call with curl and read the response envelope. - [Look up a transaction](https://docs.rarebl.com/api/tx/get): Fetch any transaction by hash on Bitcoin, Litecoin, Bitcoin Cash, Dash, Ethereum or TRON. - [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction): Build, verify, sign on your machine and broadcast, step by step. - [Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally): JavaScript, Python, PHP and shell SDKs: your keys never leave your machine. - [Track a deposit](https://docs.rarebl.com/guides/track-a-deposit): Poll tx/status until a payment reaches your confirmation threshold. - [Webhooks](https://docs.rarebl.com/guides/webhooks): Signed deliveries for blocks, confirmations, addresses and token transfers. No account. - [Market prices and fees](https://docs.rarebl.com/guides/market-data): Reference prices, FX rates and live network fees for every chain. - [Chains & units](https://docs.rarebl.com/chains-and-units): The six chains, decimals, address formats and amount conventions. - [Postman collection](https://docs.rarebl.com/collections/rarebl-api.postman_collection.json): Download the generated collection and environment and call all 42 endpoints. ## Postman collection Download [the collection](https://docs.rarebl.com/collections/rarebl-api.postman_collection.json) and [the environment](https://docs.rarebl.com/collections/rarebl-live.postman_environment.json), then import both into Postman. Both are generated from these docs. ## Support Email [hello@rarebl.com](mailto:hello@rarebl.com) or message [WhatsApp](https://wa.me/233551648687). Include the request (chain and hash or address) and the full reply. ## Requests and responses Reads take `GET` with query parameters (v1 reads also accept a JSON `POST`); v2 writes take `POST` with a JSON body. Every reply is `{status, code, message, data, go}` with a code from API200 to API500. See [Responses & codes](https://docs.rarebl.com/responses-and-codes). ## Authentication None: no keys and no accounts. Clients are limited per IP, by rate class. `X-API-KEY` is reserved and ignored. See [Authentication](https://docs.rarebl.com/authentication) and [Rate limits](https://docs.rarebl.com/rate-limits). ## Next steps - [Quickstart](https://docs.rarebl.com/quickstart) - [API reference](https://docs.rarebl.com/api) - [Track a deposit until it confirms](https://docs.rarebl.com/guides/track-a-deposit) - [Coverage and limits](https://docs.rarebl.com/limits-today) Docs: https://docs.rarebl.com --- # Quickstart Make your first Rarebl Open API calls in about two minutes: list the chains, check a transaction and read a price. The API serves **Bitcoin, Litecoin, Bitcoin Cash, Dash, Ethereum and TRON** from Rarebl's own nodes. There is no account and no API key. ## Before you start - `curl`, or any HTTP client. Every example below also has JavaScript, Python, PHP or shell tabs. - Nothing else: no sign-up and no key. Clients are limited per IP address ([Rate limits](https://docs.rarebl.com/rate-limits)). The base URL is `https://api.rarebl.com/open//`. Reads are `GET` with query parameters (or `POST` with a JSON body); most take a `chain`: `btc`, `ltc`, `bch`, `dash`, `eth` or `tron`. ## Step 1 — List the chains Ask which chains exist and whether they are healthy. ```bash cURL curl "https://api.rarebl.com/open/stats/chains" ``` ```js JavaScript const res = await fetch('https://api.rarebl.com/open/stats/chains'); const body = await res.json(); console.log(body.data.chains.map((c) => `${c.id} ${c.status} ${c.tip?.height}`)); ``` ```python Python import json, urllib.request with urllib.request.urlopen("https://api.rarebl.com/open/stats/chains") as r: body = json.load(r) print([(c["id"], c["status"], c["tip"] and c["tip"]["height"]) for c in body["data"]["chains"]]) ``` ```php PHP setTimeout(r, wait * 1000)); } throw new Error('rate limited'); } ~~~ ## Staying under the limits - **Reads:** use [Live stats](https://docs.rarebl.com/api/stats/live) to watch all chains in one call; cache what does not change; poll no faster than the data changes ([Data freshness](https://docs.rarebl.com/data-freshness)). Prefer [webhooks](https://docs.rarebl.com/guides/webhooks) or the [WebSocket](https://docs.rarebl.com/guides/websocket) to polling. - **Compute:** build once and reuse the reply; the builds an SDK makes count. Use [Estimate](https://docs.rarebl.com/api/tx/estimate) sparingly in loops. - **Write:** this is the tight one. A broadcast is 1 of 6 per minute (burst 3) and 300 per day per IP. Batch your outputs where the chain allows, send an `Idempotency-Key` so a retry is not a second broadcast, and queue payouts instead of firing them in a burst. - **Watch:** up to 100 addresses per call; 2,000 addresses per day. Watch only what you need. Address webhooks and WebSocket address channels on BTC, LTC and BCH watch for you, and count. - **Hooks:** create one subscription per thing you follow and delete the ones you no longer need (100 active per IP). - **Stream:** one connection per app, up to 3 per IP. ## Per IP, behind NAT The limit is per IP address. Many users behind one address (an office, a mobile carrier, your own backend) share the bucket. Run heavy workloads from your own server, and cache. ## Higher limits `X-API-KEY` is reserved. There are no keyed plans and no billing today; a key sent is ignored. See [Authentication](https://docs.rarebl.com/authentication). --- # Pagination Three endpoints return lists that can be paged. Each uses the style that fits its data. | Endpoint | Style | Request | Reply | | --- | --- | --- | --- | | [Latest blocks](https://docs.rarebl.com/api/block/latest) | Height cursor | `before`, `limit` (1 to 25, default 10) | `items`, `next` | | [Block](https://docs.rarebl.com/api/block/get) | Page number | `page` (default 1), `limit` (1 to 100, default 25) | `txs`, `page`, `pages`, `total` | | [Address transactions](https://docs.rarebl.com/api/address/transactions) | Opaque cursor | `cursor`, `limit` (1 to 100, default 25) | `items`, `next` | | [Mempool](https://docs.rarebl.com/api/tx/mempool) | None | `limit` (1 to 100, default 25) | `items` | ## Cursor on `before` (blocks) `next` is the **lowest height** of the page. Pass it back as `before` to get the blocks below it. `next` is `null` at block 0 (on TRON, at the first block the node holds). ~~~bash curl "https://api.rarebl.com/open/block/latest?chain=btc&limit=10" curl "https://api.rarebl.com/open/block/latest?chain=btc&limit=10&before=975843" ~~~ ## Page number (block transactions) [Block](https://docs.rarebl.com/api/block/get) pages the block's transactions. Use `page` from 1 up to `pages`. `total` is the number of transactions in the block. ~~~bash curl "https://api.rarebl.com/open/block/get?chain=btc&id=975851&page=2&limit=100" ~~~ ## Opaque cursor (address transactions) Pass `next` back as `cursor`, unchanged. Do not build or edit a cursor yourself. `next` is `null` on the last page. ~~~bash curl "https://api.rarebl.com/open/address/transactions?chain=dash&address=XgNx6VFjYhSKPxXcjcPvad61DPSQhdiJDT&limit=25" ~~~ Address history is available on DASH only today: see [Coverage and limits](https://docs.rarebl.com/limits-today). ## A paging loop ~~~js let before; const blocks = []; for (let page = 0; page < 5; page++) { const url = new URL('https://api.rarebl.com/open/block/latest'); url.search = new URLSearchParams({ chain: 'btc', limit: '50', ...(before && { before }) }); const { data } = await (await fetch(url)).json(); blocks.push(...data.items); if (data.next === null) break; before = String(data.next); } ~~~ --- # Data freshness ## Live heads: about 2 seconds The chain heads in [Live stats](https://docs.rarebl.com/api/stats/live) (tip, mempool, fee) are cached for **at most about 2 seconds**, and the reply says when each node was last asked (`checkedAt`). Polling it once every 2 seconds is as fresh as it gets. | Data | Freshness | | --- | --- | | Chain heads, mempool size, standard fee ([Live stats](https://docs.rarebl.com/api/stats/live)) | up to about 2 s old | | Mempool list ([Mempool](https://docs.rarebl.com/api/tx/mempool)) | about 3 s, shared between callers | | Fees ([Recommended fees](https://docs.rarebl.com/api/fees/recommended)) | about 5 to 15 s | | Chain list ([Chains](https://docs.rarebl.com/api/stats/chains)) | about 10 s | | Blocks, transactions and addresses deeper than the reorg depth | stable, cacheable for long | ## Confirmations `confirmations` is the number of blocks on top of the transaction's block, counting that block. It is 0 while pending. It grows as new blocks arrive, so recompute or re-fetch rather than storing it. ## Reorg depth Data more than the **reorg depth** below the chain tip is treated as settled and cached for long. The depths in use are: | Chain | Reorg depth (blocks) | | --- | --- | | BTC, LTC, BCH, DASH | 6 | | ETH | 64 | | TRON | 20 | A transaction with fewer confirmations than this can still move to another block, or disappear, in a chain reorganisation. As guidance, wait for at least the reorg depth of your chain before you treat a payment as final. The decision is yours; the table only tells you what the API considers settled. ## Chain health A chain can be `ok`, `lagging` or `down` in [Chains](https://docs.rarebl.com/api/stats/chains) and [Live stats](https://docs.rarebl.com/api/stats/live). While a node is syncing, its `tip` is `null` and the chain is `lagging`. A node that does not answer is `down`, and calls for it answer `API502`. One slow node never delays the others. ## Be kind to the tip Fetch what you need when the head changes instead of polling every endpoint. A simple pattern: poll [Live stats](https://docs.rarebl.com/api/stats/live) every 2 seconds, compare `tip.hash` for your chain, and call other endpoints only when it changes. --- # Coverage and limits Some data is not available on every chain: depending on the chain, a call can answer `API409` or `API502`, or return `null`. This page says what, and why. ## What works where | Capability | BTC | LTC | BCH | DASH | ETH | TRON | | --- | --- | --- | --- | --- | --- | --- | | Chains, live heads, blocks, transactions, status | yes | yes | yes | yes | node syncing | history from 2026-09-08 | | Address balance | `null` until watched and active | `null` until watched and active | `null` until watched and active | exact | exact | exact | | Address transactions | from the watch time | from the watch time | from the watch time | yes | as the explorer index lands | as the explorer index lands | | UTXOs | watch first | watch first | watch first | yes | n/a | n/a | | Build, estimate, assemble, broadcast, decode | yes | yes | yes | yes | node syncing | yes | | Nonce / resources | n/a | n/a | n/a | n/a | `address/nonce` | `address/resources` | | Mempool | yes (sample) | yes (sample) | yes (sample) | yes (sample) | `API409` while no pending view | yes (up to 50) | | Webhooks and WebSocket | yes | yes | yes | yes | node syncing | yes | The authoritative, live version of the chain rows is `features` in [Chains](https://docs.rarebl.com/api/stats/chains). ## The reasons, and why - **The Ethereum node is syncing.** The Ethereum node (Erigon) is still indexing, so `tx_index_pending` (`API409`) or `API502` can come from any ETH call, including build, nonce, gas and broadcast. While it syncs, the chain shows `lagging` with a `null` tip. - **`before_history_start`: TRON history starts on 2026-09-08.** The TRON node holds blocks from that date. A block height below it answers `API409`, and `historyFrom` in [Chains](https://docs.rarebl.com/api/stats/chains) shows the date. - **`history_unavailable`: the history index is in progress.** [Address transactions](https://docs.rarebl.com/api/address/transactions) works on DASH. On BTC, LTC and BCH it covers an address **from the time it was watched** (not its earlier history); ETH and TRON follow as the explorer index lands. Until then it answers `API409`. - **`not_watched`: BTC, LTC and BCH addresses need a watch** for UTXOs, exact balances and activity. A new watch is `pending` while a UTXO-set bootstrap runs, then `active`. **How long the bootstrap takes depends on the deployment and the chain:** poll [Watch addresses](https://docs.rarebl.com/api/address/watch) or [Address UTXOs](https://docs.rarebl.com/api/address/utxos) until it is active. - **`no_mempool_view`:** [Mempool](https://docs.rarebl.com/api/tx/mempool) answers this where the node has no pending transactions to show (ETH today). ## Other limits - **Rate limits** are per IP and per rate class. Broadcast and the other write calls are the tightest: 6 per minute, burst 3, 300 per day. See [Rate limits](https://docs.rarebl.com/rate-limits). - **Hosted signing** needs the header `X-Rarebl-Accept-Risk: hosted-signing`, and Try It is disabled for it on this site so a private key is never typed into a web page: [Hosted signing](https://docs.rarebl.com/guides/hosted-signing). - **Webhook deliveries** come from `167.71.28.63` ([Webhooks](https://docs.rarebl.com/guides/webhooks)). Always verify the signature as well. - **Watches** expire after 90 days without a query or webhook. Hooks: at most 100 active subscriptions per IP. - **The stream:** 3 connections per IP, 200 channel subscriptions each. - **Market data** is reference data from public feeds, not Rarebl trading rates. - **Replacing a pending ETH or TRON transaction** is not covered in v2. - Token endpoints return **known tokens** only; `holders` and `transfers` are `null` until the index exists. On TRON, TRC-10 assets are listed with null symbols. - On Litecoin, MWEB amounts are hidden: `fee` is `null` and unresolved input values are `null`. - Litecoin and Bitcoin Cash resolve input addresses and values on [Transaction](https://docs.rarebl.com/api/tx/get) only, for at most 50 inputs. Output `spent` flags are looked up for at most 50 outputs. - Mempool results on UTXO chains are a **sample**; `count` is the real size. - `X-API-KEY` is reserved: there are no keyed plans or billing. - Not part of the Open API today: contract and logs endpoints, an RPC proxy and usage reporting. These limits change as the index and the Ethereum node progress. The [Changelog](https://docs.rarebl.com/changelog) records each change. --- # Data types The shapes below are the same ones the [Rarebl Explorer](https://explorer.rarebl.com) uses, so the explorer and the API always agree. Endpoint pages refer to them by name. TypeScript notation, with `T | null` where a value can be unknown. ## ChainInfo ~~~ts type ChainInfo = { id: 'btc' | 'ltc' | 'bch' | 'dash' | 'eth' | 'tron' name: string; symbol: string; decimals: number; kind: 'utxo' | 'account' status: 'ok' | 'lagging' | 'down' // down = the node did not answer tip: { height: number; hash: string; time: string } | null historyFrom: string | null // earliest block time that can be shown features: { addressHistory: boolean; addressBalance: boolean; tokens: boolean; mempool: boolean; internalTxs: boolean } } ~~~ ## Live ~~~ts type Live = { serverTime: string chains: { id: Chain status: 'ok' | 'lagging' | 'down' tip: { height: number; hash: string; time: string } | null mempool: { count: number | null; bytes: number | null } | null fee: { unit: string; standard: string | null } | null checkedAt: string }[] } ~~~ ## BlockSummary and Block ~~~ts type BlockSummary = { height: number; hash: string; time: string; txCount: number size: number | null miner: string | null // coinbase payout address / fee recipient / TRON witness reward: string | null // native amount fees: string | null } type Block = BlockSummary & { prevHash: string | null; nextHash: string | null; confirmations: number details: DetailRow[] } ~~~ On UTXO chains `reward` is all coinbase outputs and `fees` is the reward minus the subsidy (null on Dash). On ETH, `fees` is everything users paid in the block (burnt plus priority) and `reward` is the priority fees. On TRON, `reward` is null and `fees` is the TRX burnt by the block's transactions. ## TxSummary ~~~ts type TxSummary = { hash: string; block: number | null; time: string | null status: 'success' | 'failed' | 'pending' from: string | null // UTXO: the first input's address (null for coinbase) to: string | null // UTXO: the first non-change output's address, best effort value: string // native amount moved (UTXO: total output) fee: string | null method: string | null // ETH/TRON: decoded method or "Transfer"; UTXO: "coinbase" or null inputs?: number; outputs?: number // UTXO only token?: { contract: string; symbol: string | null; amount: string | null } // first token transfer, if any } ~~~ ## Tx ~~~ts type Tx = { chain: Chain; hash: string; status: 'success' | 'failed' | 'pending' block: number | null; blockHash: string | null; time: string | null; confirmations: number value: string; fee: string | null utxo?: { size: number; vsize: number | null; weight: number | null; locktime: number; version: number rbf: boolean | null; coinbase: boolean inputs: { txid: string | null; vout: number | null; address: string | null; value: string | null; scriptType: string | null; sequence: number | null; coinbase: boolean }[] outputs: { n: number; address: string | null; value: string; scriptType: string | null; opReturn: string | null; spent: boolean | null }[] } account?: { from: string; to: string | null; contractCreated: string | null nonce: number | null; type: number | null gasLimit: string | null; gasUsed: string | null; gasPrice: string | null // Gwei for ETH maxFeePerGas: string | null; maxPriorityFeePerGas: string | null; baseFee: string | null; burntFee: string | null input: string | null; methodId: string | null; methodName: string | null tokenTransfers: TokenTransfer[] logs: { index: number; address: string; topics: string[]; data: string }[] tron?: { contractType: string; energyUsed: number | null; energyFee: string | null; bandwidthUsed: number | null; netFee: string | null; result: string | null } } details: DetailRow[] } type TokenTransfer = { standard: 'ERC-20' | 'ERC-721' | 'ERC-1155' | 'TRC-20' | 'TRC-721' | 'TRC-10' contract: string; symbol: string | null; name: string | null; decimals: number | null from: string; to: string amount: string | null // formatted with decimals when known rawAmount: string; tokenId: string | null; logIndex: number | null } ~~~ On TRON, `gasLimit`, `gasUsed`, `gasPrice`, `maxFeePerGas`, `maxPriorityFeePerGas`, `baseFee`, `burntFee`, `nonce` and `type` are `null`; use the `tron` object instead. ## Address ~~~ts type Address = { chain: Chain; address: string; valid: boolean type: string | null // p2pkh, p2sh, p2wpkh, p2tr, cashaddr, eoa, contract, tron-account, ... balance: string | null // native balanceStatus: 'exact' | 'index_pending' // BTC/LTC/BCH: index_pending until the history index exists received: string | null; sent: string | null; txCount: number | null tokens: { contract: string; standard: string; symbol: string | null; name: string | null; decimals: number | null; balance: string }[] isContract: boolean | null history: { available: boolean; reason: string | null } details: DetailRow[] } ~~~ ## Token ~~~ts type Token = { chain: 'eth' | 'tron'; contract: string; standard: string name: string | null; symbol: string | null; decimals: number | null; totalSupply: string | null holders: number | null; transfers: number | null // null until the index exists details: DetailRow[] } ~~~ ## Fees ~~~ts type Fees = { chain: Chain; unit: string // "sat/vB" (UTXO), "Gwei" (ETH), "sun" (TRON) slow: string | null; standard: string | null; fast: string | null baseFee: string | null // ETH details: DetailRow[] // TRON energy/bandwidth prices, ETH priority fee percentiles updatedAt: string } ~~~ ## DetailRow Chain-specific overview rows. The API picks and orders them, and a client renders each by its `kind`. ~~~ts type DetailRow = { key: string; label: string; value: string | number | boolean | null kind: 'text' | 'number' | 'amount' | 'hash' | 'address' | 'block' | 'tx' | 'time' | 'bytes' | 'gas' | 'percent' | 'bool' | 'code' unit?: string // for example "BTC", "Gwei", "sat/vB" hint?: string // tooltip text } ~~~ --- # Track a deposit until it confirms You have a transaction hash, for example from a customer who says "I sent it", and you want to know when it is safe to credit. You will poll [Transaction status](https://docs.rarebl.com/api/tx/status) until the transaction reaches your confirmation threshold, then confirm the amount and the address with [Transaction](https://docs.rarebl.com/api/tx/get). ## Before you start - The `chain` the payment was sent on (`btc`, `ltc`, `bch`, `dash`, `eth` or `tron`) and the transaction hash (`id`). - A confirmation threshold per chain. The reorg depths in [Data freshness](https://docs.rarebl.com/data-freshness) are 6 for BTC, LTC, BCH and DASH, 64 for ETH and 20 for TRON. - Prefer being told instead of polling? Use a `tx.confirmations` [webhook](https://docs.rarebl.com/guides/webhooks) and skip to step 3. ## Step 1 — Ask for the status ```bash cURL curl "https://api.rarebl.com/open/tx/status?chain=btc&id=9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db" ``` ```js JavaScript const url = 'https://api.rarebl.com/open/tx/status?chain=btc&id=9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db'; const body = await (await fetch(url)).json(); console.log(body.data.status, body.data.confirmations); ``` ```python Python import rarebl c = rarebl.Rarebl() data = c.status("btc", "9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db") ``` ```bash Shell rarebl status --chain btc 9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db ``` **You should see** a small reply with the status and the confirmation count. ```json { "status": 1, "code": "API200", "message": "success", "data": { "hash": "9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db", "status": "success", "block": 975851, "confirmations": 20, "time": "2026-11-14T22:18:52Z" }, "go": null, "meta": { "requestid": "0123456789abcdef", "notices": [] } } ``` `data.status` is one of: | `status` | Meaning | What to do | | --- | --- | --- | | `pending` | Seen in the mempool, not in a block. `block` and `time` are `null`, `confirmations` is 0. | Keep polling. | | `success` | In a block. `confirmations` counts the blocks on top, including its own. | Credit once `confirmations` reaches your threshold. | | `failed` | Included in a block but reverted (ETH and TRON). | Do not credit. | ## Step 2 — Poll until it confirms Poll every few seconds, back off on errors, and stop at your threshold. Heads refresh about every 2 seconds, so polling faster gains nothing. ```js JavaScript const THRESHOLD = { btc: 6, ltc: 6, bch: 6, dash: 6, eth: 64, tron: 20 }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function waitForConfirmations(chain, id) { let delay = 4000; for (;;) { const res = await fetch(`https://api.rarebl.com/open/tx/status?chain=${chain}&id=${id}`); const body = await res.json(); if (body.code === 'API200') { const { status, confirmations } = body.data; if (status === 'failed') return { ok: false, reason: 'reverted' }; if (status === 'success' && confirmations >= THRESHOLD[chain]) return { ok: true, confirmations }; delay = 4000; } else if (body.code === 'API429') { delay = Number(res.headers.get('Retry-After') ?? 1) * 1000; } else if (body.code === 'API404') { delay = 4000; // not seen by our node yet: keep waiting } else if (body.code === 'API502' || body.code === 'API409') { delay = Math.min(delay * 2, 30000); // node syncing or index pending: back off } else { throw new Error(`${body.code}: ${body.message}`); // API400 and friends: fix the request } await sleep(delay); } } ``` ```python Python import time THRESHOLD = {"btc": 6, "ltc": 6, "bch": 6, "dash": 6, "eth": 64, "tron": 20} def wait_for_confirmations(c, chain, txid): delay = 4 while True: try: s = c.status(chain, txid) if s["status"] == "failed": return False if s["status"] == "success" and s["confirmations"] >= THRESHOLD[chain]: return True delay = 4 except rarebl.ApiError as e: if e.code == "API400": raise # fix the request delay = min(delay * 2, 30) # 404 not seen yet, 429, 502, 409: back off time.sleep(delay) ``` **You should see** the function return once the threshold is reached (or at once with a failure for a reverted transaction). ## Step 3 — Check the amount and the address `status` only tells you the transaction is final enough. Before you credit, confirm it paid the right address and amount with [Transaction](https://docs.rarebl.com/api/tx/get). ```bash cURL curl "https://api.rarebl.com/open/tx/get?chain=btc&id=9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db" ``` - **UTXO chains:** look in `data.tx.utxo.outputs[]` for an output whose `address` is yours and whose `value` matches. - **ETH and TRON:** check `data.tx.account.to` and `data.tx.value`, or for a token payment the matching entry in `data.tx.account.tokenTransfers[]`. - Compare amounts as decimal strings or with a big-number type, never as floats. **You should see** your address and the expected amount among the outputs. Only then credit the customer. ## Common errors | Reply | What it means | What to do | | --- | --- | --- | | `API404` | The hash is not found yet: not seen by our node, or wrong. | Treat it as "not seen yet" for a while, then give up after a time you choose. | | `API400` | The chain or hash is malformed. | Fix the request. | | `API409` `tx_index_pending` | ETH only: the node is still indexing old transactions. | Retry later: see [Coverage and limits](https://docs.rarebl.com/limits-today). | | `API429` | Polling too fast. | Wait `Retry-After`; one poll per pending deposit every few seconds is plenty. | | `API502` | A node is unavailable or syncing. | Back off and retry. | ## Next steps - Be pushed the events instead: [Webhooks](https://docs.rarebl.com/guides/webhooks) (`tx.confirmations`, `tx.final`). - Find a transaction from a pasted string: [Look up any address or hash](https://docs.rarebl.com/guides/look-up-anything). - Move the funds you received: [Watch addresses, get UTXOs and balances](https://docs.rarebl.com/guides/watch-addresses). - Rate limits and retries: [Errors and rate limits](https://docs.rarebl.com/guides/handle-errors). --- # Look up any address or hash Someone pastes a string: a block height, a hash, an address, a token name, even a link from another explorer. You do not know which chain it is on, or what kind of thing it is. You will call [Search](https://docs.rarebl.com/api/search/query), read the ranked matches, and open the right endpoint. ## Before you start - The string to look up. Nothing else is needed: no chain, no key. Send it as it was pasted: whitespace, quotes, `0x`, a `bitcoin:` payment link or an explorer link are handled. - Search asks the nodes of the candidate chains in parallel, with a 4 second budget in total. A slow node shows up in `unavailable`, not as an error. ## Step 1 — Search ```bash cURL curl "https://api.rarebl.com/open/search/query?q=800000" ``` ```js JavaScript const search = await (await fetch(`https://api.rarebl.com/open/search/query?q=${encodeURIComponent(q.trim())}`)).json(); if (search.code !== 'API200') throw new Error(search.message); ``` ```python Python import json, urllib.parse, urllib.request url = "https://api.rarebl.com/open/search/query?q=" + urllib.parse.quote(q.strip()) results = json.load(urllib.request.urlopen(url))["data"]["results"] ``` **You should see** one result per match, best first. A height exists on several chains, so there are several results and `ambiguous` is `true`. Each result has a `label` ready to display, a `confidence`, and a `reason`. ```json { "status": 1, "code": "API200", "message": "success", "data": { "results": [ { "chain": "btc", "type": "block", "id": "800000", "label": "Block 800000 · Bitcoin", "confidence": 0.7, "reason": "Block 800000 exists on Bitcoin", "verified": true, "path": "/btc/block/800000", "rank": 1 }, { "chain": "ltc", "type": "block", "id": "800000", "label": "Block 800000 · Litecoin", "confidence": 0.7, "reason": "Block 800000 exists on Litecoin", "verified": true, "path": "/ltc/block/800000", "rank": 2 }, { "chain": "bch", "type": "block", "id": "800000", "label": "Block 800000 · Bitcoin Cash", "confidence": 0.7, "reason": "Block 800000 exists on Bitcoin Cash", "verified": true, "path": "/bch/block/800000", "rank": 3 }, { "chain": "dash", "type": "block", "id": "800000", "label": "Block 800000 · Dash", "confidence": 0.7, "reason": "Block 800000 exists on Dash", "verified": true, "path": "/dash/block/800000", "rank": 4 } ], "best": { "chain": "btc", "type": "block", "id": "800000", "...": "the first result" }, "ambiguous": true, "query": { "input": "800000", "normalized": "800000", "kind": "height", "source": "plain", "chain": null, "type": null, "payment": null }, "suggestions": [], "unavailable": [], "mode": "full" }, "go": null, "meta": { "requestid": "0123456789abcdef", "notices": [] } } ``` (Shortened: `best` repeats `results[0]`.) ## Step 2 — Read what matched | You search for | Result | | --- | --- | | Digits (also `#975,838`, `block 975838`) | A block height on every chain where that height exists. | | 64 hex characters, with or without `0x` | A transaction or block hash. With `0x`, Ethereum first; without, every chain at once. | | `0x` + 40 hex | An ETH address (a mixed-case one must pass its EIP-55 checksum): `address`, `contract` or `token`. | | `T` + 33 base58, or 41-prefixed hex | A TRON address, or a TRC-20 contract (`token`). | | Bech32 (`bc1…`, `ltc1…`), bech32m taproot (`bc1p…`), base58 (`1`, `3`, `L`, `M`, `X`, `7`) or CashAddr (`q…`, `p…`, `bitcoincash:`) | The matching UTXO chain or chains. | | `USDT`, `tether`, `bitcoin`, `0xdac17f` | Known tokens and chains by name, symbol or contract prefix; a typo is tolerated. | | An explorer link or a `bitcoin:` / `ethereum:` payment link | The identifier inside it. | **Read `best` and `ambiguous`.** When `ambiguous` is `false` and `best` is set, open `best`. A legacy `1…` address is valid on Bitcoin and on Bitcoin Cash, but Bitcoin ranks far higher (0.92 against 0.45), so it is not ambiguous. When it is `true`, let the user pick, using `reason` and `confidence`. **No match is an empty `results` array with `API200`**, not an error: show `suggestions[].message`. ## Step 3 — Open the right endpoint Use `type` to choose the endpoint and pass `chain` and `id` along. | `type` | Call | | --- | --- | | `block` | [Block](https://docs.rarebl.com/api/block/get): `chain`, `id` | | `tx` | [Transaction](https://docs.rarebl.com/api/tx/get): `chain`, `id` | | `address` | [Address](https://docs.rarebl.com/api/address/balance): `chain`, `address` | | `contract` | [Address](https://docs.rarebl.com/api/address/balance): `chain`, `address` (an address with code) | | `token` | [Token](https://docs.rarebl.com/api/token/info): `chain`, `contract` | | `chain` | The query named a chain (`bitcoin`): [Chains](https://docs.rarebl.com/api/stats/chains) | ```js JavaScript const BASE = 'https://api.rarebl.com/open'; async function lookUp(q) { const search = await (await fetch(`${BASE}/search/query?q=${encodeURIComponent(q.trim())}`)).json(); if (search.code !== 'API200') throw new Error(search.message); const { best, ambiguous, results, suggestions } = search.data; // One clear winner: open it. Several close ones: open them all and let the user pick. const wanted = best && !ambiguous ? [best] : results; const opened = await Promise.all(wanted.filter((r) => r.type !== 'chain').map(async (r) => { const call = { block: `block/get?chain=${r.chain}&id=${r.id}&limit=1`, tx: `tx/get?chain=${r.chain}&id=${r.id}`, address: `address/balance?chain=${r.chain}&address=${r.id}`, contract: `address/balance?chain=${r.chain}&address=${r.id}`, token: `token/info?chain=${r.chain}&contract=${r.id}`, }[r.type]; return { ...r, reply: await (await fetch(`${BASE}/${call}`)).json() }; })); return { opened, suggestions }; } ``` **You should see** each opened result with the reply of the matching endpoint. ETH block and transaction ids in results carry `0x`; BCH address ids are CashAddr without the prefix. On BTC, LTC and BCH an address lookup returns `balance: null` until the address is watched: see [Watch addresses](https://docs.rarebl.com/guides/watch-addresses). ## Step 4 — Handle a miss Search tells you why nothing matched. Branch on `suggestions[].kind`, not on the text. ```json { "status": 1, "code": "API200", "message": "success", "data": { "results": [], "best": null, "ambiguous": false, "query": { "input": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNx", "normalized": "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNx", "kind": "none", "source": "plain", "chain": null, "type": null, "payment": null }, "suggestions": [ { "kind": "invalid_checksum", "chain": "btc", "message": "Looks like a Bitcoin address, but the checksum is wrong (a typo, or a missing or extra character). It could also be a Bitcoin Cash legacy address." } ], "unavailable": [], "mode": "full" }, "go": null, "meta": { "requestid": "0123456789abcdef", "notices": [] } } ``` **You should see** an empty `results` and one suggestion. `unavailable` lists chains whose node did not answer in time: tell the user the answer may be incomplete and retry. ## Instant suggestions while typing Send `mode=fast` with each keystroke (after a pause): it makes no node call, answers addresses, token names and contract prefixes at once, and returns heights and hashes with `verified: false`. Send the full lookup (the default mode) when the user presses Enter, or when the input is a complete number, hash or address. [Rarebl Explorer](https://docs.rarebl.com/explorer) does exactly this. ## Common errors | Reply | What it means | What to do | | --- | --- | --- | | `API400` with `data.param: "q"` | `q` is missing or longer than 256 characters. | Trim the input and send at most 256 characters. | | `API400` with `data.param: "mode"` or `"chain"` | The value is not one of the listed options. | Use `full` or `fast`; use a chain id such as `btc`. | | Empty `results`, a `suggestion` of kind `invalid_checksum`, `wrong_length` or `testnet` | The input looks like an address but is not valid. | Show the suggestion message; ask the user to re-copy it. | | Empty `results`, a `secret` suggestion | The input is a private or extended key. | Warn the user; the key was not searched or stored. | | Non-empty `unavailable` | A node did not answer in time. | Show what you have and retry in a moment. | | `API429` | Over 5 reads per second. | Wait `Retry-After`. | ## Next steps - Ready-made pages: [Rarebl Explorer](https://docs.rarebl.com/explorer) links straight to blocks, transactions and addresses. - Follow a transaction: [Track a deposit until it confirms](https://docs.rarebl.com/guides/track-a-deposit). - The shared response shapes: [Data types](https://docs.rarebl.com/data-types). --- # Watch addresses, get UTXOs and balances A platform that takes deposits needs to know what each address holds and then move it. You will watch your deposit addresses, list their UTXOs and balances, confirm the outputs are unspent and sweep them into one wallet. Signing is covered in [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction); this guide is about finding and choosing the funds. ## Before you start - The deposit addresses and the chain (`btc`, `ltc`, `bch`, `dash`, `eth` or `tron`). - On **BTC, LTC and BCH** an address needs a **watch** before the API can list its UTXOs or an exact balance. **DASH** works without one (it uses its node index). ETH and TRON have no UTXOs: skip to step 6. - A hot-wallet address to sweep into, and the signing key for the deposit addresses ([Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally)). ## Step 1 — Watch the addresses Start a watch for up to 100 addresses per call. The API runs a UTXO-set bootstrap, then follows new blocks and the mempool. ```bash cURL curl -X POST https://api.rarebl.com/open/address/watch -H "Content-Type: application/json" \ -d '{"chain":"btc","addresses":["bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q"]}' ``` ```js JavaScript const res = await rarebl.watch('btc', ['bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q']); // waits until active ``` ```python Python res = c.watch("btc", ["bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q"]) ``` ```php PHP $res = $c->watch('btc', ['bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q']); ``` ```bash Shell rarebl watch --chain btc bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q ``` **You should see** each address as `pending`, then `active` once its bootstrap is done (example values). ```json { "watches": [ { "address": "bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q", "status": "pending", "since": "2026-11-15T01:12:07Z" } ] } ``` The watch class allows 30 calls per minute and 2,000 addresses per day. Watches expire after 90 days without a query or webhook; any read renews them. An `address.activity` [webhook](https://docs.rarebl.com/guides/webhooks) or a WebSocket `address:*` channel on BTC, LTC or BCH starts the watch for you. ## Step 2 — Wait until the watch is active Until a watch is `active`, [Address UTXOs](https://docs.rarebl.com/api/address/utxos) answers `API409` with the reason `not_watched`. Poll every few seconds; how long the bootstrap takes depends on the chain. ```bash cURL curl "https://api.rarebl.com/open/address/utxos?chain=btc&address=bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q" ``` **You should see** this reply while the address is not watched (or not active yet), and a normal UTXO list once it is: ```json { "status": 0, "code": "API409", "message": "address is not watched yet: POST address/watch first", "data": { "reason": "not_watched", "watch": { "chain": "btc", "address": "bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q", "endpoint": "address/watch" } }, "go": null, "meta": { "requestid": "0123456789abcdef", "notices": [] } } ``` ## Step 3 — List UTXOs and the balance Once active (or straight away on DASH), list what the address holds. Use `minConf` to ignore unconfirmed deposits. ```bash cURL curl "https://api.rarebl.com/open/address/utxos?chain=dash&address=XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF&minConf=1" ``` ```js JavaScript const { utxos, balance } = await rarebl.utxos('dash', 'XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF', 1); ``` ```python Python r = c.utxos("dash", "XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF", min_conf=1) ``` ```php PHP $r = $c->utxos('dash', 'XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF', 1); ``` ```bash Shell rarebl utxos --chain dash XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF --min-conf 1 ``` **You should see** the UTXOs plus the total (a real Dash reply): ```json { "utxos": [ { "txid": "1fa1b672aa5ef13127ede15aeef61c5fe3ec8565031cd826f65e6a332baefb9c", "vout": 1, "value": "4.23877346", "valueSats": 423877346, "height": 2572425, "confirmations": 2, "scriptPubKey": "76a9147fb0a45f00c25ff1cb67753221326c7c420b8b2388ac" } ], "balance": "4.23877346", "balanceSats": 423877346 } ``` While an address is watched and active, [Address](https://docs.rarebl.com/api/address/balance) also returns an exact `balance` on BTC, LTC and BCH (`balanceStatus: "exact"`). Compare amounts as decimal strings or with a big-number type, never as floats. ## Step 4 — Check the outputs are unspent Before you spend, [Output spend status](https://docs.rarebl.com/api/tx/outspends) says whether an output is spent, by what, and whether that spend is still in the mempool. ```bash cURL curl "https://api.rarebl.com/open/tx/outspends?chain=btc&id=9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db" ``` **You should see** one entry per output: `spent: false` is spendable. ```json { "outputs": [ { "n": 0, "spent": false, "spentBy": null, "inMempool": false }, { "n": 1, "spent": true, "spentBy": null, "inMempool": false } ] } ``` ## Step 5 — Sweep with exact inputs Build a transaction that spends exactly the UTXOs you listed, to your hot wallet, with `amount: "max"` (everything less the fee). UTXO chains allow one `to` per transaction, so build one transaction per destination. ```bash cURL curl -X POST https://api.rarebl.com/open/tx/build -H "Content-Type: application/json" -d '{ "chain": "dash", "inputs": [{"txid": "1fa1b672aa5ef13127ede15aeef61c5fe3ec8565031cd826f65e6a332baefb9c", "vout": 1}], "to": [{"address": "XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM", "amount": "max"}], "priority": "standard" }' ``` ```js JavaScript const reply = await rarebl.build({ chain: 'dash', inputs: [{ txid: '1fa1b672aa5ef13127ede15aeef61c5fe3ec8565031cd826f65e6a332baefb9c', vout: 1 }], to: [{ address: 'XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM', amount: 'max' }], priority: 'standard', }); ``` ```python Python reply = c.build({ "chain": "dash", "inputs": [{"txid": "1fa1b672aa5ef13127ede15aeef61c5fe3ec8565031cd826f65e6a332baefb9c", "vout": 1}], "to": [{"address": "XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM", "amount": "max"}], "priority": "standard", }) ``` ```php PHP $reply = $c->build([ 'chain' => 'dash', 'inputs' => [['txid' => '1fa1b672aa5ef13127ede15aeef61c5fe3ec8565031cd826f65e6a332baefb9c', 'vout' => 1]], 'to' => [['address' => 'XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM', 'amount' => 'max']], 'priority' => 'standard', ]); ``` **You should see** the same reply as in [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction): `unsigned` and `toSign`, with a single output that has no change. Verify, sign, assemble and broadcast it with the steps there. Inputs are verified unspent with the node, so a stale input fails with `inputs_spent` or `inputs_unknown`. For a **payout** from a wallet use `from` instead of `inputs` and let the API choose the UTXOs; set `change` to the address that should receive the change (the default is the first `from` address). Choose the fee: | Option | When | | --- | --- | | `priority: "slow"`, `"standard"` or `"fast"` | Let the API pick from its estimator (targets of 24, 6 and 2 blocks on Bitcoin). | | `feeRate: "12"` | A fixed rate in sat/vB. | | `rbf: true` (the default on BTC and LTC) | Allows a later replacement if the fee turns out too low: [Stuck transactions](https://docs.rarebl.com/guides/stuck-transactions). | Use [Recommended fees](https://docs.rarebl.com/api/fees/recommended) to see current levels and [Estimate](https://docs.rarebl.com/api/tx/estimate) to price a specific transaction. Many small inputs make a large transaction: consolidate when fees are low. ## Step 6 — ETH and TRON deposits There are no UTXOs. Read balances with [Address](https://docs.rarebl.com/api/address/balance) and [Address tokens](https://docs.rarebl.com/api/address/tokens), the nonce with [Address nonce](https://docs.rarebl.com/api/address/nonce) (ETH) or the energy and bandwidth with [Address resources](https://docs.rarebl.com/api/address/resources) (TRON), then build a transfer from the deposit address (that address's key signs). ```bash cURL curl "https://api.rarebl.com/open/address/resources?chain=tron&address=TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL" ``` **You should see** the account's resources (a real TRON reply). ```json { "energy": { "limit": 0, "used": 0, "available": 0 }, "bandwidth": { "free": 600, "staked": 0, "used": 158, "available": 442 }, "frozen": [] } ``` A deposit address that holds only a token needs ETH (or TRX) to pay for the sweep: send the gas first, wait for it to confirm, then sweep. Token transfers: [Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally). ## Common errors | Reply | What it means | What to do | | --- | --- | --- | | `API409` `not_watched` | BTC, LTC or BCH: the address is not watched and active. | Start a watch (step 1) and poll until `active`. | | `API400` on `address/utxos` | ETH and TRON have no UTXOs, or the address is malformed. | Use balances and tokens instead. | | `API422` `inputs_spent`, `inputs_unknown` | An input was spent, or the node does not know it. | Re-list the UTXOs and build again. | | `API422` `insufficient_funds`, `dust_output` | The inputs cannot cover the amount plus the fee, or an output is too small. | Add inputs, or sweep with `amount: "max"`. | | `API429` | Watch: 30 per minute and 2,000 addresses per day. Compute: 2 per second. | Wait `Retry-After`; watch only what you need. | ## Next steps - Sign and send the sweep: [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction) or [Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally). - Be told about deposits instead of polling: [Webhooks](https://docs.rarebl.com/guides/webhooks). - Confirm one deposit: [Track a deposit until it confirms](https://docs.rarebl.com/guides/track-a-deposit). - A sweep that does not confirm: [Stuck transactions](https://docs.rarebl.com/guides/stuck-transactions). --- # Webhooks Webhooks push events to your server instead of you polling: new blocks, transaction confirmations, address activity and token transfers. You will create a subscription, receive and verify the deliveries, and then manage it. There are **no accounts**: each subscription has its own management token and signing secret. ## Before you start - A public **`https://`** endpoint on port **443 or 8443** that you control. Private, loopback, link-local and reserved IPs are refused when you create the subscription and again on every delivery, and redirects are not followed. - A place to store two secrets for each subscription (an environment variable or a secret store). - Optional: an SDK for verifying signatures: [JavaScript](https://docs.rarebl.com/sdks/js), [Python](https://docs.rarebl.com/sdks/python), [PHP](https://docs.rarebl.com/sdks/php) or [shell](https://docs.rarebl.com/sdks/shell). ## Step 1 — Create a subscription Choose the event `type` and what to follow. This example follows one transaction until it is final. ```bash cURL curl -X POST https://api.rarebl.com/open/hooks/create -H "Content-Type: application/json" -d '{ "type": "tx.confirmations", "chain": "btc", "txid": "9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db", "url": "https://example.com/hooks/rarebl", "confirmations": 6 }' ``` ```js JavaScript const sub = await rarebl.request('POST', '/open/hooks/create', { type: 'tx.confirmations', chain: 'btc', txid: '9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db', url: 'https://example.com/hooks/rarebl', confirmations: 6, }); ``` ```python Python sub = c.post("/open/hooks/create", { "type": "tx.confirmations", "chain": "btc", "txid": "9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db", "url": "https://example.com/hooks/rarebl", "confirmations": 6, }) ``` **You should see** the subscription with its `id`, `manageToken` and `signingSecret` (the values here are examples). ```json { "id": "hk_3f9a2c71e0b84d56", "manageToken": "mt_7c1d0e94a2b64f3880c5e1a9d27b6f40", "signingSecret": "whsec_b81f4a9c20de7536a1c4f08e93d2b715", "type": "tx.confirmations", "chain": "btc", "confirmations": 6, "status": "active" } ``` **Store `manageToken` and `signingSecret` now: both are shown once.** - `manageToken` authorizes every other hooks call. Send it in the header `X-Hook-Token`, never in a URL. - `signingSecret` is the key of the HMAC on every delivery. The event types: | Type | Needs | Fires | | --- | --- | --- | | `block.new` | nothing | every new block, and `block.reorged` on a reorganisation | | `tx.confirmations` | `txid` | at 0 (seen) and 1, then at each threshold up to `confirmations` (0 to 100, default 6); finally `tx.final` at the chain's reorg depth. Also `tx.dropped` (gone for 24 hours) and `tx.replaced` (RBF, with the new txid) | | `address.activity` | `address` | in the mempool and at each confirmation threshold. BTC, LTC and BCH addresses are watched automatically | | `token.transfer` | `address` or `contract` | ETH and TRON token transfers | [Create a webhook](https://docs.rarebl.com/api/hooks/create) is in the hooks rate class: 10 calls per minute, at most 100 active subscriptions per IP. ## Step 2 — Receive deliveries Rarebl `POST`s the event to your URL. Read the **raw request body**: you need the exact bytes to verify the signature. | Header | Meaning | | --- | --- | | `X-Rarebl-Event-Id` | A unique event id. Use it to deduplicate. | | `X-Rarebl-Event-Type` | For example `tx.confirmations`. | | `X-Rarebl-Signature` | `t=,v1=`: the signature to verify. | The body is `{id, type, chain, created, data, apiVersion: "2"}`. The contents of `data` depend on the event type; the values here are examples. ```json { "id": "evt_01hx9k2p4m", "type": "tx.confirmations", "chain": "btc", "created": 1763160492, "data": { "txid": "9dedeb8092d6f026d235e3beb24337ba5657635a38cc949f70c0f28ea15e30db", "confirmations": 1, "blockHeight": 975851 }, "apiVersion": "2" } ``` ## Step 3 — Verify the signature `v1` is `HMAC-SHA256(signingSecret, t + "." + body)` in hex, where `t` is the timestamp from the header and `body` is the raw body. Reject a bad signature, and reject old timestamps (the SDKs allow 300 seconds). ```js JavaScript import express from 'express'; import { webhook } from './rarebl.mjs'; const app = express(); app.post('/hooks/rarebl', express.raw({ type: '*/*' }), (req, res) => { // the RAW body, not parsed JSON if (!webhook.verify(process.env.RAREBL_SIGNING_SECRET, req.body, req.get('X-Rarebl-Signature'))) return res.sendStatus(401); const event = JSON.parse(req.body); res.sendStatus(200); // answer first, then process queue.push(event); }); ``` ```python Python import os, rarebl from flask import Flask, request, abort app = Flask(__name__) @app.post("/hooks/rarebl") def hook(): if not rarebl.webhook.verify(os.environ["RAREBL_SIGNING_SECRET"], request.get_data(), request.headers.get("X-Rarebl-Signature", ""), 300): abort(401) event = request.get_json(force=True) queue.put(event) # process after answering return "", 200 ``` ```php PHP ` and the subscription `id`. | Call | Use | | --- | --- | | [Get](https://docs.rarebl.com/api/hooks/get) | Read the subscription. | | [Update](https://docs.rarebl.com/api/hooks/update) | Change the URL or threshold. | | [Pause](https://docs.rarebl.com/api/hooks/pause), [Resume](https://docs.rarebl.com/api/hooks/resume) | Stop and restart deliveries. After 20 failures, fix your endpoint and resume. | | [Replay](https://docs.rarebl.com/api/hooks/replay) | Re-send a past event by `eventId`, for example after an outage. | | [Delete](https://docs.rarebl.com/api/hooks/delete) | Remove it when you are done. | ```bash cURL curl -X POST https://api.rarebl.com/open/hooks/pause -H "Content-Type: application/json" \ -H "X-Hook-Token: $MANAGE_TOKEN" -d '{"id": "hk_3f9a2c71e0b84d56"}' curl -X POST https://api.rarebl.com/open/hooks/replay -H "Content-Type: application/json" \ -H "X-Hook-Token: $MANAGE_TOKEN" -d '{"id": "hk_3f9a2c71e0b84d56", "eventId": "evt_01hx9k2p4m"}' ``` In **Try It** on this site, the id and token from a create call are kept in your browser and prefilled on these pages (clear them with Reset saved Try It values in the settings menu). ## Common errors | Reply | What it means | What to do | | --- | --- | --- | | `API400` on create | The URL is not `https://` on 443 or 8443, or resolves to a private address. | Use a public HTTPS endpoint. | | `API404` | The subscription id does not exist (or was deleted). | Check the id; create a new subscription. | | An error on a manage call | The `X-Hook-Token` header is missing or is not the `manageToken` of that subscription. | Send the `manageToken` from create, in the header. | | `API429` | The hooks class: 10 per minute. | Wait `Retry-After` seconds. | | Many failed deliveries | Your endpoint is slow, answers non-2xx or has a bad certificate. | Check [Deliveries](https://docs.rarebl.com/api/hooks/deliveries), fix it, then Resume and Replay. | | 100 active subscriptions | The per-IP limit. | Delete the ones you no longer need. | ## Next steps - One subscription per thing you follow; delete it when done. - Many addresses on BTC, LTC and BCH: an `address.activity` webhook watches them for you ([Watch addresses](https://docs.rarebl.com/guides/watch-addresses)). - For live dashboards prefer the [WebSocket stream](https://docs.rarebl.com/guides/websocket). - Credit deposits: [Track a deposit until it confirms](https://docs.rarebl.com/guides/track-a-deposit). --- # Live data over WebSocket Polling works, but a WebSocket gives you new blocks, fee changes and address activity as they happen, over one connection. You will connect to `wss://api.rarebl.com/ws`, subscribe to channels, handle events, answer the heartbeat and reconnect safely. Every channel, event and limit is in the reference: [WebSocket stream](https://docs.rarebl.com/api/websocket). ## Before you start - A client that speaks WebSocket: a browser or Node 22+ (global `WebSocket`), or Python with `pip install websockets`. - Nothing else: the stream is keyless and limited per IP (3 connections, 200 channel subscriptions per connection). - For events you must not miss while offline, use [webhooks](https://docs.rarebl.com/guides/webhooks) too: a WebSocket has no replay. ## Step 1 — Connect Open one connection and share it across your app. ```js JavaScript const ws = new WebSocket('wss://api.rarebl.com/ws'); ws.onopen = () => console.log('connected'); ``` ```python Python import asyncio, json, websockets async def main(): async with websockets.connect("wss://api.rarebl.com/ws") as ws: print("connected") # steps 2 and 3 go here asyncio.run(main()) ``` **You should see** the connection open. Nothing is sent until you subscribe. ## Step 2 — Subscribe to channels Send one `subscribe` message with the channels you want. ```js JavaScript ws.onopen = () => ws.send(JSON.stringify({ op: 'subscribe', channels: ['live', 'blocks:btc', 'fees:eth', 'address:btc:bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q'], })); ``` ```python Python await ws.send(json.dumps({ "op": "subscribe", "channels": ["live", "blocks:btc", "fees:eth", "address:btc:bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q"], })) ``` **You should see** a confirmation listing what was accepted and, for anything refused, a `rejected` entry with a reason. ```json { "op": "subscribed", "channels": ["live", "blocks:btc", "fees:eth", "address:btc:bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q"], "rejected": [] } ``` The channels: | Channel | What you get | | --- | --- | | `live` | The heads of every chain (the latest is replayed when you subscribe). | | `blocks:` | New blocks of a chain, and reorganisations. | | `fees:` | Fee updates. | | `address::
` | Activity on an address. On BTC, LTC and BCH the address is **watched automatically**. | | `tx::` | Progress of one transaction. | | `token::
`, `token::c:` | Token transfers of an address or a contract on `eth` or `tron`. | ## Step 3 — Handle events and the heartbeat Events arrive as `{"channel","type","data","ts"}`. The server sends `{"op":"ping"}` every **20 seconds**: answer `{"op":"pong"}` (any message you send counts as life; after 70 seconds of silence the socket is closed). ```js JavaScript ws.onmessage = (event) => { const m = JSON.parse(event.data); if (m.op === 'ping') return ws.send(JSON.stringify({ op: 'pong' })); if (m.op === 'error') return console.error(m.code, m.message); if (m.channel) queue.push(m); // hand work to a queue: never block this handler }; ``` ```python Python async for raw in ws: m = json.loads(raw) if m.get("op") == "ping": await ws.send(json.dumps({"op": "pong"})) elif m.get("op") == "error": print(m["code"], m["message"]) elif "channel" in m: queue.put_nowait(m) # hand work to a queue: never block this loop ``` **You should see** events such as a new block (example values below; the shape of `data` follows [Data types](https://docs.rarebl.com/data-types) and the webhook events in [Webhooks](https://docs.rarebl.com/guides/webhooks)). Event types: `block.new`, `block.reorged`, `address.activity`, `tx.confirmations`, `tx.final`, `tx.dropped`, `tx.replaced`, `token.transfer`, `live` and `fees`. ```json { "channel": "blocks:btc", "type": "block.new", "data": { "height": 975870, "hash": "…" }, "ts": "2026-11-15T01:09:33Z" } ``` To stop a channel, send `{"op":"unsubscribe","channels":["fees:eth"]}`. ## Step 4 — Reconnect with back-off Subscriptions do not survive a dropped connection, so resubscribe on every reconnect. Back off, and add jitter if you run many clients. ```js JavaScript const CHANNELS = ['live', 'blocks:btc']; let delay = 1000; function connect() { const ws = new WebSocket('wss://api.rarebl.com/ws'); ws.onopen = () => { delay = 1000; ws.send(JSON.stringify({ op: 'subscribe', channels: CHANNELS })); }; ws.onmessage = onMessage; ws.onclose = () => { setTimeout(connect, delay + Math.random() * 500); delay = Math.min(delay * 2, 30000); }; } connect(); ``` ```python Python async def run(): delay = 1 while True: try: async with websockets.connect("wss://api.rarebl.com/ws") as ws: delay = 1 await ws.send(json.dumps({"op": "subscribe", "channels": CHANNELS})) async for raw in ws: handle(json.loads(raw), ws) except (OSError, websockets.ConnectionClosed): await asyncio.sleep(delay) delay = min(delay * 2, 30) ``` **You should see** the stream resume after a network blip. While you were disconnected you may have missed events: catch up with [Live stats](https://docs.rarebl.com/api/stats/live), [Block](https://docs.rarebl.com/api/block/get) or [Transaction status](https://docs.rarebl.com/api/tx/status). ## Step 5 — Stay within the limits | Limit | Value | | --- | --- | | Connections | 3 per IP. Share one connection across your app; the 4th is closed (code 1008). | | Subscriptions | 200 channels per connection. | | Slow clients | A client with 256 unacknowledged messages is closed (code 1013). Do not block the handler. | | Client messages | More than 60 in 10 seconds closes the socket (1008); incoming frames are capped at 16 kB. | | Heartbeat | The server pings every 20 s; answer `{"op":"pong"}`. | | Watches | Address channels on BTC, LTC and BCH watch the address, which counts against 2,000 addresses per day ([Rate limits](https://docs.rarebl.com/rate-limits)). | ## WebSocket or webhook? | | WebSocket | Webhook | | --- | --- | --- | | Direction | You connect to Rarebl | Rarebl calls your server | | Best for | Dashboards, wallets in a browser, live fees | Servers that must not miss an event | | Delivery guarantee | None: events while disconnected are missed | Retried for 24 hours, up to 20 failures | | Setup | One connection | A public HTTPS endpoint | ## Common errors | Symptom | What it means | What to do | | --- | --- | --- | | `{"op":"error","code","message"}` | The server refused a message. | Match on `code`, fix the message. | | `rejected` entries in `subscribed` | A channel was invalid or over the limit. | Check the channel name and your count. | | Closed with 1008 | A fourth connection, or too many messages. | Reuse one connection; slow down. | | Closed with 1013 | You were too slow to read. | Process events on a queue and reconnect. | | Closed after 70 s silent | The pong was missing. | Answer every `ping` with `pong`. | ## Next steps - Reference: [WebSocket stream](https://docs.rarebl.com/api/websocket). - Server-to-server delivery with retries: [Webhooks](https://docs.rarebl.com/guides/webhooks). - Poll instead: [Live stats](https://docs.rarebl.com/api/stats/live) and [Market data and fees](https://docs.rarebl.com/guides/market-data). --- # Market data and fees Show a fiat value next to a crypto amount, draw a price chart and display the current network fee. You will use the market endpoints for **reference prices and FX rates** aggregated from public sources, and [Live stats](https://docs.rarebl.com/api/stats/live) and [Recommended fees](https://docs.rarebl.com/api/fees/recommended) for fees. > **These are reference prices, not Rarebl trading rates.** Rarebl does not guarantee that you can buy or sell at these prices, and they are not an offer. Do not use them to settle a trade. ## Before you start - Nothing to install. These are read-class calls: 5 per second, burst 20 per IP, so cache results for a few seconds instead of calling per page view. - A decimal type for money. Chain amounts are exact decimal strings and `price` is one too. ## Step 1 — Get a price [Prices](https://docs.rarebl.com/api/market/price) takes the **median of the fresh sources** and excludes any source more than 2% from the median. Quotes are `usd`, `eur`, `gbp`, `ghs` and `ngn` (the last four are derived from the dollar price through the FX rates). ```bash cURL curl "https://api.rarebl.com/open/market/price?assets=BTC,ETH"e=usd" ``` ```js JavaScript const { prices } = await rarebl.marketPrice(['BTC', 'ETH'], 'usd'); // or fetch the URL above ``` ```python Python prices = c.price(["BTC", "ETH"], "usd") ``` ```php PHP $prices = $c->price(['BTC', 'ETH'], 'usd'); ``` ```bash Shell rarebl price BTC,ETH --quote usd ``` **You should see** one entry per asset (the Bitcoin entry shortened to three sources). ```json { "asset": "BTC", "quote": "USD", "price": "63421.45", "change24h": "-7.1155", "sources": [ { "id": "binance", "pair": "BTC/USD", "price": 63449.83, "volume": 11.75039 }, { "id": "kraken", "pair": "BTC/USD", "price": 63426.4, "volume": 5752.38744852 }, { "id": "coinbase", "pair": "BTC/USD", "price": 63421.45, "volume": 16529.56926348 } ], "stale": false, "updatedAt": "2026-11-15T01:00:51.309Z" } ``` | Field | Meaning | | --- | --- | | `asset`, `quote` | The symbol and the quote currency (upper case in replies). | | `price` | A decimal string. | | `change24h` | The percent change over 24 hours as a string such as `"-1.25"`, or `null`. | | `sources` | The exchanges that went into the median. An entry with `excluded: true` was an outlier; `usdtProxy: true` marks a USDT market used because fewer than two USD or USDC markets were fresh. | | `stale` | True when there were not enough fresh sources, or two sources disagree by more than 4%. Show the price with a warning, or hide it. | | `updatedAt` | When it was computed. | ## Step 2 — Convert with FX [FX](https://docs.rarebl.com/api/market/fx) returns currency rates against a base, with the source and update time. ```bash cURL curl "https://api.rarebl.com/open/market/fx?base=usd"es=ghs,ngn" ``` **You should see** the rates keyed by quote currency. ```json { "rates": { "GHS": "11.747527", "NGN": "1352.072165" }, "source": "er-api", "updatedAt": "2026-11-15T00:37:02.017Z" } ``` ## Step 3 — Draw a chart [History](https://docs.rarebl.com/api/market/history) returns OHLC candles (`1m`, `1h` or `1d`; at most 1,000 per call). Use `from` and `to` to page through longer ranges. Candle `volume` is `null`, and a fiat quote other than USD converts candles at the current FX rate, which is an approximation. ```bash cURL curl "https://api.rarebl.com/open/market/history?asset=BTC"e=USD&interval=1h&from=1763161200" ``` **You should see** candles, oldest first. ```json { "candles": [ { "ts": "2026-11-14T23:00:00.000Z", "open": "64136.4", "high": "64174.2", "low": "64068.4", "close": "64139.5", "volume": null }, { "ts": "2026-11-15T00:00:00.000Z", "open": "64123.32", "high": "64469.5", "low": "63430.38", "close": "63478.34", "volume": null } ] } ``` ## Step 4 — Show the network fee [Live stats](https://docs.rarebl.com/api/stats/live) gives every chain's head, mempool and fee in one call. It refreshes about every 2 seconds, so poll no faster than that. ```bash cURL curl "https://api.rarebl.com/open/stats/live" ``` ```js JavaScript async function refresh() { const { data } = await (await fetch('https://api.rarebl.com/open/stats/live')).json(); for (const c of data.chains) { const el = document.querySelector(`[data-fee="${c.id}"]`); el.textContent = c.fee?.standard != null ? `${c.fee.standard} ${c.fee.unit}` : 'unavailable'; } } refresh(); setInterval(refresh, 2000); ``` **You should see** an entry per chain with a `fee`. A chain that is `lagging` or `down` has `fee: null`: show "unavailable" rather than a number. ```json { "id": "btc", "status": "ok", "tip": { "height": 975869, "hash": "000000000000000000018a26062659ab25520a26b803d3e4ec9568a85c3f4235", "time": "2026-11-15T01:00:09Z" }, "mempool": { "count": 55876, "bytes": 22836693 }, "fee": { "unit": "sat/vB", "standard": "0.12" }, "checkedAt": "2026-11-15T01:01:02Z" } ``` When the user can choose a speed, call [Recommended fees](https://docs.rarebl.com/api/fees/recommended) for that chain. ```bash cURL curl "https://api.rarebl.com/open/fees/recommended?chain=btc" ``` **You should see** slow, standard and fast rates and their unit (`details` omitted here). ```json { "chain": "btc", "unit": "sat/vB", "slow": "0.12", "standard": "0.12", "fast": "0.12", "baseFee": null, "updatedAt": "2026-11-15T01:01:02Z" } ``` The unit depends on the chain, so always show it next to the number: - **UTXO chains:** `sat/vB`. The three speeds map to estimator targets of 24, 6 and 2 blocks on Bitcoin. Bitcoin Cash has no estimator on its node, so all three values are the relay minimum. - **ETH:** `Gwei`, with `baseFee` set and priority fee percentiles in `details`. - **TRON:** `sun`; `standard` is the energy price in sun per energy unit, and `slow` and `fast` are `null`. For a UTXO transaction the fee is about `rate × vsize`: a 142 vB transaction at 1 sat/vB costs about 142 sats. ## Step 5 — Show a fiat value, with attribution ```js JavaScript const { data } = await (await fetch('https://api.rarebl.com/open/market/price?assets=BTC"e=GHS')).json(); const p = data.prices[0]; if (!p.stale) { const fiat = (Number(balanceBtc) * Number(p.price)).toFixed(2); // for display only: use a decimal library if you store it label.textContent = `${fiat} GHS · reference price`; } ``` - Say where the number comes from: show "Reference price from public exchange feeds via Rarebl" (or similar) near it, and state that it is not a trading rate. - Exchange data comes from Binance, Coinbase Exchange, Kraken, Bitstamp, Bitfinex, OKX, Bybit and KuCoin; FX from ExchangeRate-API (open.er-api.com) and the ECB. - Check `stale` and `updatedAt`, and decide what your product does when the data is old. - Map a token contract to a price symbol with [Assets](https://docs.rarebl.com/api/market/assets) (for example USDT on `eth` and on `tron`), then ask for that symbol. [Tickers](https://docs.rarebl.com/api/market/tickers) lists the individual exchange tickers behind a price. ## Common errors | Reply | What it means | What to do | | --- | --- | --- | | `API400` with `data.param` | An unknown asset, quote or interval, or a bad range. | Check the symbol against [Assets](https://docs.rarebl.com/api/market/assets). | | `API429` | More than 5 reads per second. | Cache for a few seconds and honour `Retry-After`. | | `stale: true` in a price | Not enough fresh sources, or they disagree. | Show a warning or hide the price. | | `fee: null` in live stats | That chain is `lagging` or `down`. | Show "unavailable" and keep the last value. | | `API502` | A node is unavailable. | Keep the last value and retry after a longer pause. | ## Next steps - Decide a fee for a payment: [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction) and [Estimate](https://docs.rarebl.com/api/tx/estimate). - Get fee updates pushed: [Live data over WebSocket](https://docs.rarebl.com/guides/websocket) (`fees:`). - Errors and retries: [Errors and rate limits](https://docs.rarebl.com/guides/handle-errors). --- # Send a transaction Send a payment on any chain in four calls: **build** the transaction, **verify** what the API returned, **sign** the digests on your machine, then **assemble and broadcast**. Your private key never leaves your code. The examples use a real Dash build; the flow is identical on BTC, LTC, BCH, ETH and TRON. If you only want the shortcut, an SDK does all four steps in one `transfer()` call: [Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally). ## Before you start - An address with funds on the chain, and its private key kept locally (an environment variable, a file or an HSM, never in source control). - BTC, LTC and BCH senders must be **watched** first: [Watch addresses, get UTXOs and balances](https://docs.rarebl.com/guides/watch-addresses). DASH, ETH and TRON need no watch. - An SDK ([JavaScript](https://docs.rarebl.com/sdks/js), [Python](https://docs.rarebl.com/sdks/python), [PHP](https://docs.rarebl.com/sdks/php) or [shell](https://docs.rarebl.com/sdks/shell)), or your own secp256k1 signing code. - A small test amount for the first run. A broadcast cannot be undone. ## Step 1 — Build the transaction Ask the API for an unsigned transaction. It picks the inputs, adds change and prices the fee; you choose `priority` (`slow`, `standard`, `fast`) or a fixed `feeRate`. ```bash cURL curl -X POST https://api.rarebl.com/open/tx/build -H "Content-Type: application/json" -d '{ "chain": "dash", "from": ["XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF"], "to": [{"address": "XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM", "amount": "0.01"}], "priority": "standard" }' ``` ```js JavaScript import { Rarebl } from './rarebl.mjs'; const rarebl = new Rarebl(); // https://api.rarebl.com const reply = await rarebl.build({ chain: 'dash', from: ['XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF'], to: [{ address: 'XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM', amount: '0.01' }], priority: 'standard', }); ``` ```python Python import rarebl c = rarebl.Rarebl() # https://api.rarebl.com reply = c.build({ "chain": "dash", "from": ["XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF"], "to": [{"address": "XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM", "amount": "0.01"}], "priority": "standard", }) ``` ```php PHP build([ 'chain' => 'dash', 'from' => ['XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF'], 'to' => [['address' => 'XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM', 'amount' => '0.01']], 'priority' => 'standard', ]); ``` ```bash Shell rarebl build --chain dash --from XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF \ --to XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM --amount 0.01 --priority standard > build.json ``` **You should see** the `unsigned` transaction, the `toSign` digests and an `expiresAt` (hex shortened here). ```json { "unsigned": { "chain": "dash", "format": "utxo-v1", "tx": "0200000001139f6185036ea872d9e52bd7424ba9165f31b6479556b021fe4b8f3f42e8087a0100000000ffffffff02…00000000", "inputs": [ { "txid": "7a08e8423f8f4bfe21b0569547b6315f16a94b42d72be5d972a86e0385619f13", "vout": 1, "address": "XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF", "value": "4.23879226", "valueSats": "423879226", "scriptType": "p2pkh", "sequence": 4294967295, "prevTx": "0200000001…00000000" } ], "outputs": [ { "address": "XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM", "value": "0.01", "valueSats": "1000000", "change": false }, { "address": "XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF", "value": "4.22879", "valueSats": "422879000", "change": true } ], "fee": "0.00000226", "feeSats": "226", "vsize": 226, "feeRate": "1" }, "toSign": [ { "index": 0, "address": "XnL1FvTKqe6aWfB8NrvgbUMuWCEA3dNxfF", "digest": "60f09e680a8d4d7442ec09e2e89181c568f596c6200c6a2d2f4aa15a4a8fc6b1", "algorithm": "ecdsa-secp256k1", "sighashType": 1, "scriptType": "p2pkh" } ], "expiresAt": "2026-11-15T00:31:27Z" } ``` On legacy p2pkh inputs (BTC, LTC, DASH) each input also carries `prevTx`, the raw previous transaction (hex shortened here). The signature of such an input does not commit to its amount, so the SDKs read the amount from `prevTx` after checking that it hashes to the input's `txid`. ETH and TRON take a single `from` and `to` plus an optional `token`; their `unsigned.format` is `eth-1559-v1` or `tron-v1`. See [Build](https://docs.rarebl.com/api/tx/build) for every field. ## Step 2 — Verify the reply A digest is 32 bytes: you cannot see what it commits to. If anything between you and the API returned a transaction that pays someone else, your valid signature would authorize it. So check before you sign: 1. **Recompute each digest** from `unsigned` (rules at the end of this page). 2. **Check the outputs**: the recipient and amount are what you asked for, and any change goes back to you. 3. **Check the fee** against a cap you set. The SDKs do all three, and signing refuses to run without an expectation: you must say who is paid. Pass the recipient and amount, and a fee cap if the defaults do not suit you. ```js JavaScript import { describeBuild } from './rarebl.mjs'; const expect = { to: [{ address: 'XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM', amount: '0.01' }], maxFeeSats: 5000 }; console.log(describeBuild(reply, { expect })); // what is really being paid; throws VerifyError on a mismatch ``` ```python Python expect = {"to": {"address": "XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM", "amount": "0.01"}, "maxFeeSats": 5000} rarebl.verify_build(reply, expect=expect) # raises VerifyError on a mismatch ``` ```php PHP $expect = ['to' => ['address' => 'XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM', 'amount' => '0.01'], 'maxFeeSats' => 5000]; Rarebl\Signer::verifyBuild($reply, null, $expect); // throws VerifyException on a mismatch ``` The shell tool verifies inside `rarebl sign` (next step) when you pass `--expect-to` and `--expect-amount`. **You should see** no error. A mismatch raises `VerifyError` before anything is signed. Never turn verification off. Even when `expect` names no fee cap, the SDKs apply one: | Chain | Default cap | Override in `expect` | | --- | --- | --- | | BTC, LTC | 1000 sat/vB | `maxFeeRate`, or an absolute `maxFee` / `maxFeeSats` | | BCH, DASH | 50 sat/byte | `maxFeeRate`, or an absolute `maxFee` / `maxFeeSats` | | ETH | `maxFeePerGas` at most 500 gwei, total fee at most 0.05 ETH, tip at most max(5 gwei, `maxFeePerGas` / 2) | `maxFeePerGas`, `maxPriorityFeePerGas` (wei), `maxFee` | | TRON | `feeLimit` at most 150 TRX | `maxFeeLimit` (sun) | They also refuse a UTXO transaction with a non-zero locktime (unless `allowLocktime: true`), a version other than 1 or 2, a repeated input, or a sighash type other than SIGHASH_ALL (`0x41`, with FORKID, is accepted on BCH only). ## Step 3 — Sign the digests locally Sign each digest with the key of the input's `address`. The SDK verifies again, signs and returns one signature per digest. ECDSA is RFC 6979 deterministic and low-S; Schnorr (p2tr) uses fresh random auxiliary data unless you pass `deterministic` for reproducible output. ```js JavaScript import { signBuild } from './rarebl.mjs'; const signatures = signBuild(reply, [process.env.DASH_KEY], { expect }); // expect is required ``` ```python Python import os signatures = rarebl.sign_build(reply, os.environ["DASH_KEY"], expect=expect) # expect is required ``` ```php PHP $signatures = Rarebl\Signer::signBuild($reply, getenv('DASH_KEY'), ['expect' => $expect]); // expect is required ``` ```bash Shell rarebl sign build.json --key-env DASH_KEY \ --expect-to XiM9Afap6UEb5LDkcWrg2YyA28hifkZZKM --expect-amount 0.01 > sigs.json ``` **You should see** one `{index, signature, publicKey}` per digest (the values here are examples). ```json [ { "index": 0, "signature": "3044022047ac8e87…2f0220217f36a4…dcb01", "publicKey": "03ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a26873" } ] ``` If you sign without an SDK (an HSM, another language), produce the signature form for the chain: DER plus the sighash byte for UTXO ECDSA, 64 or 65 bytes for Schnorr (p2tr), and 65 bytes `r ‖ s ‖ v` for ETH and TRON. ## Step 4 — Assemble the signed transaction Send `unsigned` back unchanged with your signatures. The API checks every signature against its digest and key, and that the key matches the input address, then returns the signed transaction. Leave `broadcast` false for now so you can inspect it. ```bash cURL curl -X POST https://api.rarebl.com/open/tx/assemble -H "Content-Type: application/json" -d '{ "unsigned": { …the unsigned object from step 1… }, "signatures": [ { "index": 0, "signature": "3044…01", "publicKey": "03ad…6873" } ], "broadcast": false }' ``` ```js JavaScript const { signed } = await rarebl.assemble(reply.unsigned, signatures, { broadcast: false }); ``` ```python Python signed = c.assemble(reply["unsigned"], signatures, broadcast=False)["signed"] ``` ```php PHP $signed = $c->assemble($reply['unsigned'], $signatures, false)['signed']; ``` ```bash Shell rarebl assemble build.json sigs.json > signed.json ``` **You should see** `signed` with the transaction hex and its `txid`, and `broadcast: null`. ```json { "signed": { "chain": "dash", "hex": "0200000001139f…88ac00000000", "txid": "…" }, "broadcast": null } ``` Optional: decode the hex with [Decode](https://docs.rarebl.com/api/tx/decode) to see the outputs one more time before you send. ## Step 5 — Broadcast Send it with [Broadcast](https://docs.rarebl.com/api/tx/broadcast), with an `Idempotency-Key` so a retry can never send twice (the same key, or the same txid, within 24 hours returns the first result). Or set `broadcast: true` in step 4 to do both in one call. ```bash cURL curl -X POST https://api.rarebl.com/open/tx/broadcast -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-2026-11-15-0001" \ -d '{"chain": "dash", "hex": "0200000001139f…88ac00000000"}' ``` ```js JavaScript const result = await rarebl.broadcast('dash', signed.hex, { idempotencyKey: 'payout-2026-11-15-0001' }); ``` ```python Python result = c.broadcast("dash", signed["hex"], idempotency_key="payout-2026-11-15-0001") ``` ```php PHP $result = $c->broadcast('dash', $signed['hex'], 'payout-2026-11-15-0001'); ``` ```bash Shell rarebl assemble build.json sigs.json --broadcast # assemble and broadcast in one step ``` **You should see** the txid and `accepted: true`. ```json { "txid": "…", "accepted": true, "alreadyKnown": false } ``` Broadcast is in the **write** rate class: 6 per minute (burst 3) and 300 per day per IP, so queue payouts instead of firing them in a burst. UTXO broadcasts run the node's `testmempoolaccept` first at its default maximum fee rate; you may lower it with `maxFeeRate` (sat/vB) but never raise it. ## Step 6 — Wait for confirmation Follow the transaction with [Transaction status](https://docs.rarebl.com/api/tx/status), or let a `tx.confirmations` [webhook](https://docs.rarebl.com/guides/webhooks) tell you. Confirmation thresholds: [Data freshness](https://docs.rarebl.com/data-freshness). ```bash cURL curl "https://api.rarebl.com/open/tx/status?chain=dash&id=" ``` **You should see** `status: "pending"` with `confirmations: 0`, then `"success"` once it is in a block. If it stays pending for a long time: [Stuck transactions](https://docs.rarebl.com/guides/stuck-transactions). ## Local or hosted signing Everything above signs on your side. Rarebl also offers hosted signing, where `tx/sign` and `tx/send` sign for you with a private key you send in the request. | | Sign locally (this guide, SDKs) | Hosted signing | | --- | --- | --- | | Where the key is | Only on your machine | Sent to Rarebl over TLS for each request, in an envelope that is single-use and expires within 300 seconds | | Who signs | Your code or the SDK | Rarebl's signer, in memory | | Checking the digests | You, with `verifyBuild` and `expect` | The signer signs only what the envelope names: the digests (`tx/sign`) or the recipients and amounts (`tx/send`) | | Extra requirement | None | Header `X-Rarebl-Accept-Risk: hosted-signing` | | Rate class | Build and assemble are compute; broadcast is write | Write | Signing locally keeps keys on your side. Hosted signing is covered in [Hosted signing](https://docs.rarebl.com/guides/hosted-signing); the SDKs are on the [JavaScript](https://docs.rarebl.com/sdks/js), [Python](https://docs.rarebl.com/sdks/python), [PHP](https://docs.rarebl.com/sdks/php) and [shell](https://docs.rarebl.com/sdks/shell) pages. ## How the digests are made The SDKs implement these rules and check them in step 2. If you sign by hand, so must you. | Script type | Digest | Algorithm | | --- | --- | --- | | p2pkh (BTC, LTC, DASH) | legacy sighash, SIGHASH_ALL (1) | `ecdsa-secp256k1` | | p2wpkh, p2sh-p2wpkh (BTC, LTC) | BIP143 | `ecdsa-secp256k1` | | p2tr key path (BTC) | BIP341, SIGHASH_DEFAULT (0) | `schnorr-bip340` | | BCH | BIP143-style with FORKID, sighashType `0x41` | `ecdsa-secp256k1` | | ETH | `keccak256(0x02 ‖ rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]))` | `ecdsa-secp256k1-recoverable` | | TRON | `txID` = sha256 of the `raw_data` protobuf bytes | `ecdsa-secp256k1-recoverable` | For ETH also check that `chainId` is 1 and that `to`, `value` and `data` are what you meant (a token transfer calls `a9059cbb` with the recipient and raw amount). For TRON check that `sha256(raw_data_hex)` equals `txID`. ## Common errors | Reason | What it means | What to do | | --- | --- | --- | | `API409` `not_watched` | BTC, LTC or BCH: the `from` address is not watched and active. | [Watch it](https://docs.rarebl.com/guides/watch-addresses) and wait for `active`. | | `API422` `insufficient_funds` | The inputs cannot cover the amount plus the fee. | Fund the address, lower the amount or use `amount: "max"`. | | `API422` `dust_output` | An output is below the dust limit. | Raise the amount or drop the tiny output. | | `API422` `inputs_spent`, `inputs_unknown` | An input is already spent, or the node does not know it. | Check [Output spend status](https://docs.rarebl.com/api/tx/outspends) and build again. | | `API422` `bad_signature` | A signature does not verify against its digest and key. | Sign the digest from this build, with the right key. | | `API403` `key_mismatch` | The key does not derive the input's address. | Use the key of the address in `toSign`. | | `VerifyError`: missing `prevTx` or wrong amount | A legacy p2pkh input (BTC, LTC, DASH) has no `prevTx`, or its `prevTx` does not hash to the input's `txid` or does not carry the amount `tx/build` stated. Nothing is signed. | Build again and use the reply as it is. If you pass your own, give `prevTxs` (`prev_txs`) as raw transactions keyed by txid. | | `VerifyError`: fee above the cap | The fee is above the default or the `expect` cap. | Lower `feeRate` or `priority`, or raise the cap in `expect` if the fee is intended. | | `InputError`: `expect` required | Signing was called without `expect` (or without `expect.to`). | Pass the recipient and amount. `unsafeNoExpect` (`--unsafe-no-expect`) exists for signing without pinning the recipient; the fee caps still apply. | | `API422` `fee_too_low`, `fee_too_high` | The fee is below what the network relays, or above the safety limit. | Change `feeRate` or `priority`. | | `API422` `nonce_too_low`, `chain_id_mismatch` | ETH: the nonce is behind the pending nonce, or the chain id is not 1. | Build again. | | `API422` `tx_expired` | TRON: the transaction expired before it was broadcast. | Build, sign and broadcast again. | | `API422` `node_rejected` | The node refused it; `data.detail` has its message. | Read `detail`, for example `min relay fee not met`. | | `API429` | A rate limit. | Wait `Retry-After` seconds. | The full list is in [Responses & codes](https://docs.rarebl.com/responses-and-codes). ## Next steps - One call instead of four: [Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally), including ETH and USDT on TRON. - Move many deposits: [Watch addresses, get UTXOs and balances](https://docs.rarebl.com/guides/watch-addresses). - A payment that does not confirm: [Stuck transactions](https://docs.rarebl.com/guides/stuck-transactions). - Handling failures in production: [Errors and rate limits](https://docs.rarebl.com/guides/handle-errors). --- # Sign locally with the SDKs The SDKs turn the whole [send flow](https://docs.rarebl.com/guides/send-a-transaction) into one `transfer()` call. They ask the API for UTXOs, nonce and fees, then build, verify and sign **on your machine**, and ask the API to broadcast. Your private key is never sent over the network. You will download an SDK, load a key, and send BTC, ETH or USDT on TRON. ## Before you start - An address with a small balance on the chain you want to try, and its private key (hex or WIF) in an environment variable or a file. - For ETH and TRON, a little ETH or TRX for fees, even when you send a token. - JavaScript (Node 18+ or a browser), Python 3.9+, PHP 8.1+ with `ext-gmp`, or bash with `curl`, `jq` and Python 3.9+. - Run signing on a machine you control. The pure-language crypto is correct but not constant-time, so avoid shared hosting. ## Step 1 — Download the SDK Download the single file and check its SHA-256 against `https://docs.rarebl.com/sdk/SHA256SUMS`. Each SDK page lists the checksum: [JavaScript](https://docs.rarebl.com/sdks/js), [Python](https://docs.rarebl.com/sdks/python), [PHP](https://docs.rarebl.com/sdks/php), [shell](https://docs.rarebl.com/sdks/shell). ```bash JavaScript curl -fsSLO https://docs.rarebl.com/sdk/js/rarebl.mjs curl -s https://docs.rarebl.com/sdk/SHA256SUMS | grep " js/rarebl.mjs"; sha256sum rarebl.mjs ``` ```bash Python curl -fsSLO https://docs.rarebl.com/sdk/python/rarebl.py curl -s https://docs.rarebl.com/sdk/SHA256SUMS | grep " python/rarebl.py"; sha256sum rarebl.py ``` ```bash PHP curl -fsSLO https://docs.rarebl.com/sdk/php/Rarebl.php curl -s https://docs.rarebl.com/sdk/SHA256SUMS | grep " php/Rarebl.php"; sha256sum Rarebl.php ``` ```bash Shell curl -fsSLO https://docs.rarebl.com/sdk/shell/rarebl && chmod +x rarebl curl -s https://docs.rarebl.com/sdk/SHA256SUMS | grep " shell/rarebl"; sha256sum rarebl ``` **You should see** the two hashes match. Put the file next to your script. ## Step 2 — Create or load a key Generate a key locally, or load one you already have. Keys never leave this process. ```js JavaScript import { Rarebl, keys } from './rarebl.mjs'; const me = keys.generate('btc', 'p2wpkh'); // or keys.fromPrivate('btc', process.env.BTC_WIF, 'p2wpkh') console.log(me.address); // fund this address, keep me.privateKey secret const rarebl = new Rarebl(); // https://api.rarebl.com ``` ```python Python import os, rarebl me = rarebl.keys.generate("btc", "p2wpkh") # or rarebl.keys.fromPrivate("btc", key) print(me["address"]) # fund this address, keep me["privateKey"] secret c = rarebl.Rarebl() # https://api.rarebl.com ``` ```php PHP btc.json # prints the new address and key: keep this file private jq -r .privateKey btc.json > btc.key; chmod 600 btc.key jq -r .address btc.json ``` **You should see** a new address. Send it a small amount and wait for a confirmation before you continue. Keep keys out of source control and out of command lines. ## Step 3 — Send BTC On BTC, LTC and BCH the sender is watched first so the API can list its UTXOs. The JavaScript and shell SDKs let you start that explicitly; Python and PHP do it automatically the first time. See [Watch addresses](https://docs.rarebl.com/guides/watch-addresses). ```js JavaScript await rarebl.watch('btc', me.address); // waits until the watch is active const txid = await rarebl.transfer({ chain: 'btc', from: me.address, to: 'bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm', amount: '0.0015', key: me.privateKey, priority: 'standard', // or feeRate: 12 (sat/vB) }); console.log(txid); ``` ```python Python txid = c.transfer(chain="btc", from_=me["address"], to="bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm", amount="0.0015", key=me["privateKey"], priority="standard") # or fee_rate=12 (sat/vB) print(txid) ``` ```php PHP $txid = $c->transfer([ 'chain' => 'btc', 'from' => $me['address'], 'to' => 'bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm', 'amount' => '0.0015', 'key' => $me['privateKey'], 'priority' => 'standard', ]); echo $txid, "\n"; ``` ```bash Shell rarebl watch --chain btc "$(jq -r .address btc.json)" rarebl transfer --chain btc --from "$(jq -r .address btc.json)" --to bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm \ --amount 0.0015 --key-file btc.key --priority standard # prints the txid ``` **You should see** the transaction id (a 64-character hex string). Behind that one call, the SDK built the transaction, recomputed every digest, checked that it pays exactly what you asked (recipient, amount, change back to you, a fee cap), signed, assembled and broadcast. If anything differs it raises `VerifyError` before signing. ## Step 4 — Send ETH or a token on Ethereum Ethereum needs a nonce and gas. The API supplies the nonce and fee suggestions; you can pin them with `maxFee` (a cap in wei). For an ERC-20 token, `token` is the contract address and `amount` is in whole token units. ```js JavaScript const txid = await rarebl.transfer({ chain: 'eth', from: '0xYourAddress', to: '0xRecipient', amount: '25', token: '0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT; leave out `token` to send ETH key: process.env.ETH_KEY, }); ``` ```python Python txid = c.transfer(chain="eth", from_="0xYourAddress", to="0xRecipient", amount="25", token="0xdac17f958d2ee523a2206206994597c13d831ec7", key=os.environ["ETH_KEY"], max_fee=21000 * 40 * 10**9) # cap in wei ``` ```php PHP $txid = $c->transfer([ 'chain' => 'eth', 'from' => '0xYourAddress', 'to' => '0xRecipient', 'amount' => '25', 'token' => '0xdac17f958d2ee523a2206206994597c13d831ec7', 'key' => getenv('ETH_KEY'), ]); ``` ```bash Shell rarebl transfer --chain eth --from 0xYourAddress --to 0xRecipient --amount 25 \ --token 0xdac17f958d2ee523a2206206994597c13d831ec7 --key-env ETH_KEY ``` **You should see** the transaction hash. The sender needs ETH for gas; a deposit address that holds only USDT cannot send until it has some. ## Step 5 — Send USDT on TRON `token` is the TRC-20 contract. A TRON transfer uses energy and bandwidth; what the account lacks is paid by burning TRX, capped by `feeLimit` (in sun, 1 TRX = 1,000,000 sun). Check what the account has with [Address resources](https://docs.rarebl.com/api/address/resources). ```js JavaScript const txid = await rarebl.transfer({ chain: 'tron', from: 'TYourAddress', to: 'TRecipient', amount: '25', token: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', // USDT-TRC20 key: process.env.TRON_KEY, // 64 hex characters feeLimit: 15_000_000, }); ``` ```python Python txid = c.transfer(chain="tron", from_="TYourAddress", to="TRecipient", amount="25", token="TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", key=os.environ["TRON_KEY"], max_fee=100_000_000) # feeLimit cap in sun ``` ```php PHP $txid = $c->transfer([ 'chain' => 'tron', 'from' => 'TYourAddress', 'to' => 'TRecipient', 'amount' => '25', 'token' => 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', 'key' => getenv('TRON_KEY'), 'maxFee' => 100000000, // feeLimit cap in sun ]); ``` ```bash Shell rarebl transfer --chain tron --from TYourAddress --to TRecipient --amount 25 \ --token TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t --key-env TRON_KEY --max-fee 100000000 ``` **You should see** the transaction id. TRON transactions expire (see `expiresAt` in the build reply), so sign and broadcast promptly; the SDK does both in one call. ## Step 6 — Check the result ```bash cURL curl "https://api.rarebl.com/open/tx/status?chain=btc&id=" ``` ```python Python print(c.status("btc", txid)) ``` **You should see** `status: "pending"` with 0 confirmations, then `"success"`. For a push instead of polling use a `tx.confirmations` [webhook](https://docs.rarebl.com/guides/webhooks). ## Good to know | Topic | What to do | | --- | --- | | Token decimals | USDT and USDC are built in. For any other token pass `decimals` explicitly: the SDK never takes decimals from the server. | | Fee caps | `max_fee` / `maxFee` caps the fee in base units (sats, wei or sun). The SDK refuses a build that exceeds it. Without one, SDK 0.2.0 applies defaults: 1000 sat/vB on BTC and LTC, 50 sat/byte on BCH and DASH, ETH `maxFeePerGas` 500 gwei and 0.05 ETH in total, TRON `feeLimit` 150 TRX. | | Pinned recipient | `transfer()` always checks the build against the recipient, amount and token you passed. Signing a build yourself (`signBuild`, `sign_build`, `Signer::signBuild`, `rarebl sign`) needs an `expect` with the recipient too: [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction). | | Legacy inputs | On BTC, LTC and DASH p2pkh inputs the SDK reads the input amount from the `prevTx` that `tx/build` delivers, after checking that it hashes to the input's txid. | | Reproducible signatures | ECDSA is always deterministic. Schnorr (p2tr) adds random auxiliary data; pass `deterministic` (`--deterministic` in the shell) for byte-identical output. | | Not broadcasting yet | Pass `broadcast=False` (`--no-broadcast` in the shell) to get the signed transaction, inspect it with [Decode](https://docs.rarebl.com/api/tx/decode), then [Broadcast](https://docs.rarebl.com/api/tx/broadcast). | | Token contracts | USDT on `eth`: `0xdac17f958d2ee523a2206206994597c13d831ec7`. USDT on `tron`: `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t`. Map others with [Assets](https://docs.rarebl.com/api/market/assets). | ## Common errors The SDKs raise typed errors that carry `code`, `reason` and `detail`. | Error | What it means | What to do | | --- | --- | --- | | `VerifyError` | The API's reply did not match what you asked for, a digest could not be recomputed, the fee is above the cap, or a legacy p2pkh input lacks its `prevTx`. Raised before any signature. | Do not sign. Retry, and report it with the request id (`meta.requestid` or `X-Request-Id`). | | `InputError` | Signing a build yourself without `expect` (or without `expect.to`). `transfer()` does not raise it. | Pass the recipient and amount in `expect`, as in [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction). | | `API409` `not_watched` | BTC, LTC or BCH sender not active yet. | Wait for the watch, then retry. | | `API422` `insufficient_funds` | Balance cannot cover the amount plus the fee (or gas). | Fund the address, or lower the amount. | | `API422` `nonce_too_low`, `tx_expired` | ETH nonce behind, or a TRON transaction expired. | Run `transfer()` again. | | `API422` `node_rejected` | The node refused it; read `detail`. | Often a fee below the relay minimum: raise `priority` or `feeRate`. | | `API429` | Write limit: 6 per minute, burst 3. | Wait `Retry-After`; queue your payouts. | More: [Responses & codes](https://docs.rarebl.com/responses-and-codes). ## Next steps - See what `transfer()` does under the hood, or sign with an HSM: [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction). - Sweep many deposits: [Watch addresses, get UTXOs and balances](https://docs.rarebl.com/guides/watch-addresses). - Follow confirmations: [Webhooks](https://docs.rarebl.com/guides/webhooks). - Fix a payment that does not confirm: [Stuck transactions](https://docs.rarebl.com/guides/stuck-transactions). --- # Stuck transactions: RBF and CPFP A UTXO transaction can sit unconfirmed because its fee rate is too low for the current mempool. There are two fixes: **replace it** with a higher-fee version (RBF), or **pull it through** with a child that pays a high fee (CPFP). RBF works on Bitcoin and Litecoin; CPFP works on every UTXO chain. ## Before you start - The txid and chain of the stuck transaction, and the key that signed it. - To use RBF the original must have signalled replaceability. `rbf` defaults to **true** in [Build](https://docs.rarebl.com/api/tx/build), so transactions you built signal it. - Ethereum and TRON: v2 builds ETH transactions with the pending nonce and has no call to replace a pending transaction with the same nonce. Wait, or send again once it is confirmed or dropped. ## Step 1 — Find out what is going on ```bash cURL curl "https://api.rarebl.com/open/tx/status?chain=btc&id=" curl "https://api.rarebl.com/open/tx/outspends?chain=btc&id=" curl "https://api.rarebl.com/open/fees/recommended?chain=btc" ``` ```python Python print(c.status("btc", txid)) print(c.outspends("btc", txid)) ``` ```bash Shell rarebl status --chain btc ``` **You should see** `status: "pending"` with 0 confirmations. Then compare: - The transaction's fee rate (`feeRate` in [Transaction](https://docs.rarebl.com/api/tx/get)) against the `fast` and `standard` rates from [Recommended fees](https://docs.rarebl.com/api/fees/recommended). - [Output spend status](https://docs.rarebl.com/api/tx/outspends): whether its outputs are spent in the mempool, which matters for CPFP. - A `tx.dropped` [webhook](https://docs.rarebl.com/guides/webhooks) fires when a transaction has been gone for 24 hours, and `tx.replaced` fires when it is replaced (with the new txid). ## Step 2 — Choose a fix | Fix | Use when | Chains | | --- | --- | --- | | **RBF**: rebuild with the same inputs and a higher `feeRate` | You built the transaction and it signalled `rbf`. | BTC, LTC | | **CPFP**: spend an unconfirmed output of the stuck transaction with a high fee | You own an output of it (usually your change), or RBF is not possible. | All UTXO chains | ## Step 3 — Replace it (RBF) Build again with the **same inputs** and recipient and a **higher `feeRate`** (above the original, with room for the network's replacement rules). Spending a different set of inputs creates a second payment instead of a replacement. ```bash cURL curl -X POST https://api.rarebl.com/open/tx/build -H "Content-Type: application/json" -d '{ "chain": "btc", "inputs": [{"txid": "", "vout": 1}], "to": [{"address": "", "amount": "0.0015"}], "change": "", "feeRate": "40", "rbf": true }' ``` ```python Python build = c.build({"chain": "btc", "inputs": [{"txid": input_txid, "vout": 1}], "to": [{"address": dest, "amount": "0.0015"}], "change": addr, "feeRate": "40", "rbf": True}) sigs = rarebl.sign_build(build, key, expect={"to": {"address": dest, "amount": "0.0015"}}) # verifies every digest first c.assemble(build["unsigned"], sigs, broadcast=True) ``` `expect` is required when you sign, so name the same recipient and amount as the original payment; change may only return to your own input addresses. The default fee cap is 1000 sat/vB (BTC, LTC), so a replacement above that needs `maxFeeRate` in `expect`. **You should see** a new `unsigned` and `toSign` with a higher `feeRate`. Verify, sign, assemble and broadcast as in [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction); the new transaction replaces the old one in the mempool. A `tx.replaced` webhook, or [Transaction status](https://docs.rarebl.com/api/tx/status), tells you which txid won. ## Step 4 — Or pull it through (CPFP) Spend an **unconfirmed output of the stuck transaction** in a new transaction with a high fee, so miners include both together. The change output of your own payment is the usual candidate. 1. Find the output (for example `n: 1`, your change) in [Transaction](https://docs.rarebl.com/api/tx/get) and confirm it is unspent with [Output spend status](https://docs.rarebl.com/api/tx/outspends). 2. Build with **exact `inputs`** pointing at that output and a higher `feeRate`, sending to an address you control: ```json { "chain": "btc", "inputs": [{"txid": "", "vout": 1}], "to": [{"address": "bc1q…your own address", "amount": "max"}], "feeRate": "60" } ``` **You should see** a build with one input and no change. Sign it with `expect` set to your own address and `amount: "max"` (a `max` spend must leave no change output), then broadcast it. If the node does not accept the unconfirmed input, the build fails with `inputs_unknown` or `inputs_spent`. ## Step 5 — Choose the rate The miner looks at the parent and the child **together**. To reach a target rate, the child's fee must cover its own size and the parent's shortfall: `child fee = target × (parent vsize + child vsize) − parent fee`. For example, a 141 vB parent that paid 705 sats (5 sat/vB) and a 110 vB child, aiming for 40 sat/vB overall, needs a child fee of `40 × 251 − 705 = 9,335` sats, about 85 sat/vB on the child. [Estimate](https://docs.rarebl.com/api/tx/estimate) gives you the child's size before you commit. ## Prevention - Use `priority: "fast"` or a fixed `feeRate` from [Recommended fees](https://docs.rarebl.com/api/fees/recommended) for payouts that matter. - Keep `rbf: true` (the default) on Bitcoin and Litecoin. - Follow every payment with a `tx.confirmations` [webhook](https://docs.rarebl.com/guides/webhooks) so you notice stuck ones quickly. ## Common errors | Reason | What it means | What to do | | --- | --- | --- | | `API422` `inputs_spent` | The input was already spent, maybe by the replacement or by the transaction itself. | Check [Output spend status](https://docs.rarebl.com/api/tx/outspends) and the new txid. | | `API422` `inputs_unknown` | The node does not know that unconfirmed input. | Wait for the parent to propagate, or rebuild. | | `API422` `node_rejected` | The node refused the replacement, for example `insufficient fee` or `min relay fee not met`. | Raise `feeRate`: a replacement must pay more than the original. | | `API422` `fee_too_high` | The fee is above the safety limit. | Lower `feeRate`. | | `API429` | Compute: 2 per second; write: 6 per minute. | Wait `Retry-After`. | ## Next steps - [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction): the verify, sign and broadcast steps. - [Webhooks](https://docs.rarebl.com/guides/webhooks): `tx.confirmations`, `tx.dropped`, `tx.replaced`. - [Errors and rate limits](https://docs.rarebl.com/guides/handle-errors). --- # Hosted signing Hosted signing lets Rarebl sign for you: you send a private key with the request, and the signer builds, signs or generates keys in memory and forgets the key. It is a good fit for a quick script or an environment that cannot run an SDK. The key reaches Rarebl over TLS, wrapped in an encrypted envelope that is bound to one chain, one operation and what may be signed; signing locally with an SDK keeps keys on your side ([Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally)). The three endpoints: | Endpoint | What it does | | --- | --- | | [tx/sign](https://docs.rarebl.com/api/tx/sign) | Signs an `unsigned` transaction from `tx/build`. Never broadcasts. | | [tx/send](https://docs.rarebl.com/api/tx/send) | Builds, signs and broadcasts in one call. | | [address/new](https://docs.rarebl.com/api/address/new) | Generates a new keypair in the signer and returns it once. | Every hosted request needs the header `X-Rarebl-Accept-Risk: hosted-signing`. Without it the API answers `API403` with reason `risk_not_acknowledged`. ## Before you start - A key to sign with, as a WIF or 64-character hex string, for an address that holds funds. - SDK 0.2.0 or later ([JavaScript](https://docs.rarebl.com/sdks/js), [Python](https://docs.rarebl.com/sdks/python) or [PHP](https://docs.rarebl.com/sdks/php)), which builds the envelope, or your own X25519 and AES-GCM code (format at the end of step 3). Envelope v1, which earlier SDK versions produced, is refused with `bad_envelope`. - Try It is disabled for these endpoints on this site, so that a private key is never typed into a web page. Use your own server. ## Step 1 — Fetch the signer key The signer publishes a public X25519 key. Encrypt your private key to it so that only the signer process can read it; the web server, the firewall and PHP only see ciphertext. ```bash cURL curl https://api.rarebl.com/open/tx/signer-key ``` ```js JavaScript import { Rarebl, envelope } from './rarebl.mjs'; const rarebl = new Rarebl(); const signer = await rarebl.signerKey(); // {kid, alg, publicKey, expiresAt}: pass it whole to envelope.encrypt ``` ```python Python import os, rarebl c = rarebl.Rarebl() signer = c.signer_key() # {kid, alg, publicKey, expiresAt}: pass it whole to envelope.encrypt ``` ```php PHP signerKey(); // ['kid' => ..., 'publicKey' => ..., 'expiresAt' => ...] ``` **You should see** a key id, the algorithm and the public key. Keys rotate: the previous `kid` stays valid for 7 days after its successor was created, and a key is refused 7 days after its `expiresAt`. Fetch the key when you need it instead of hard-coding it. ```json { "status": 1, "code": "API200", "message": "success", "data": { "kid": "k20261115-defd9c", "alg": "X25519", "publicKey": "wL-KbhcrI3WWpG1nv8sNAoiWUVTyw9yMxfn1_jvoxT4", "expiresAt": "2026-12-15T00:12:34.514Z" }, "go": null, "meta": { "requestid": "0123456789abcdef", "notices": [] } } ``` ## Step 2 — Build the transaction (tx/sign only) For [tx/sign](https://docs.rarebl.com/api/tx/sign) you sign a transaction you built with [tx/build](https://docs.rarebl.com/api/tx/build), and the envelope names the digests of that build. Build it, then check the reply as in [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction) before you let anyone sign it. With [tx/send](https://docs.rarebl.com/api/tx/send) there is nothing to build first: skip to step 3 and name the recipients instead. ```js JavaScript import { describeBuild } from './rarebl.mjs'; const to = 'bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm'; const reply = await rarebl.build({ chain: 'btc', from: [from], to: [{ address: to, amount: '0.0015' }], priority: 'standard' }); console.log(describeBuild(reply, { expect: { to: [{ address: to, amount: '0.0015' }] } })); // throws VerifyError on a mismatch ``` ```python Python to = "bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm" reply = c.build({"chain": "btc", "from": [from_addr], "to": [{"address": to, "amount": "0.0015"}], "priority": "standard"}) rarebl.verify_build(reply, expect={"to": {"address": to, "amount": "0.0015"}}) # raises VerifyError on a mismatch ``` ```php PHP $to = 'bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm'; $reply = $c->build(['chain' => 'btc', 'from' => [$from], 'to' => [['address' => $to, 'amount' => '0.0015']], 'priority' => 'standard']); Rarebl\Signer::verifyBuild($reply, null, ['to' => ['address' => $to, 'amount' => '0.0015']]); // throws VerifyException on a mismatch ``` **You should see** no error, and `reply.toSign` with one digest per input. ## Step 3 — Encrypt the key into an envelope The envelope carries your private key together with the rules the signer enforces. It is bound to: - **the chain** (`chain`), which must be the chain of the request; - **the operation** (`op`): `sign` for `tx/sign`, `send` for `tx/send`; - **what may be signed**: for `sign`, the exact list of `toSign` digests (`digests`); for `send`, the recipients and amounts (`bind.to`, as base-unit integers, plus `bind.token` for a token transfer); - **a lifetime**: it expires within 300 seconds (`exp`) and opens once (`jti`). The SDKs fill in `exp` and `jti`. For `tx/sign`, name the digests of the build you checked: ```js JavaScript const digests = reply.toSign.map((t) => t.digest); // the digests you just verified const encrypted = await envelope.encrypt(signer, { privateKeys: [process.env.BTC_WIF], op: 'sign', chain: 'btc', digests, }); ``` ```python Python digests = [t["digest"] for t in reply["toSign"]] # the digests you just verified encrypted = rarebl.envelope.encrypt(signer, { "privateKeys": [os.environ["BTC_WIF"]], "op": "sign", "chain": "btc", "digests": digests, }) # needs the `cryptography` package for AES-GCM ``` ```php PHP $digests = array_column($reply['toSign'], 'digest'); // the digests you just verified $encrypted = Envelope::encrypt($signer, [ 'privateKeys' => [getenv('BTC_WIF')], 'op' => 'sign', 'chain' => 'btc', 'digests' => $digests, ]); ``` For `tx/send`, name the recipients. `bindOf` converts whole-unit amounts to base units (satoshi, wei, sun or token units): ```js JavaScript const bind = envelope.bindOf('btc', [{ address: to, amount: '0.0015' }]); // token transfers: { token: '0x…' } as the third argument const encrypted = await envelope.encrypt(signer, { privateKeys: [process.env.BTC_WIF], op: 'send', chain: 'btc', bind, }); ``` ```python Python bind = rarebl.envelope.bind_of("btc", {"address": to, "amount": "0.0015"}) # token transfers: token="0x…" encrypted = rarebl.envelope.encrypt(signer, { "privateKeys": [os.environ["BTC_WIF"]], "op": "send", "chain": "btc", "bind": bind, }) ``` ```php PHP $bind = Envelope::bindOf('btc', ['address' => $to, 'amount' => '0.0015']); // token transfers: the contract as the third argument $encrypted = Envelope::encrypt($signer, [ 'privateKeys' => [getenv('BTC_WIF')], 'op' => 'send', 'chain' => 'btc', 'bind' => $bind, ]); ``` **You should see** a long base64url string. Send it as `keys.encrypted`. If you build the envelope yourself, it is `base64url(JSON {v:2, kid, epk, iv, ct, tag})` with every field base64url without padding. Generate a fresh ephemeral X25519 key `epk`, derive the shared secret with the signer's public key, then HKDF-SHA256 with an empty salt and the info `"rarebl-signer-v2" || epk || recipientPub` (the ASCII label, your 32-byte `epk`, the signer's 32-byte public key, all raw) to get a 32-byte key. Encrypt with AES-256-GCM, a 12-byte IV and the AAD `info || utf8(kid)`; `ct` is the ciphertext and `tag` the 16-byte tag. The plaintext is JSON: ```json { "v": 2, "privateKeys": [""], "exp": 1794000000, "jti": "<32 lowercase hex>", "op": "sign", "chain": "btc", "digests": [""] } ``` `exp` is in unix seconds and must be at most 300 seconds ahead. For `op: "send"` replace `digests` with `"bind": {"to": [{"address": "…", "amount": "150000"}]}` (at most 10 recipients; `"max"` is accepted on UTXO chains) and add `"token": ""` inside `bind` for a token transfer. A plain `{"privateKeys": [...]}` body is still accepted, but it carries no binding: nothing limits what the key signs, and anything on the path can read it. Prefer the envelope. ## Step 4 — Sign, or build, sign and send Use [tx/sign](https://docs.rarebl.com/api/tx/sign) with the `unsigned` transaction from step 2, then broadcast the result yourself. Or use [tx/send](https://docs.rarebl.com/api/tx/send) for one call that builds, signs and broadcasts: the body is a `tx/build` body plus `keys`. ```bash cURL curl -X POST https://api.rarebl.com/open/tx/send \ -H "Content-Type: application/json" \ -H "X-Rarebl-Accept-Risk: hosted-signing" \ -d '{ "chain": "btc", "from": ["bc1q7dszvzdj8lk8dd4ssad94m0t2acunaf48j455q"], "to": [{"address": "bc1qanfrvw5lunlemvm09dthnxmwknal0fw0q7jfnm", "amount": "0.0015"}], "priority": "standard", "keys": { "encrypted": "BASE64URL_ENVELOPE" } }' ``` ```js JavaScript const hdr = { 'X-Rarebl-Accept-Risk': 'hosted-signing' }; // tx/send: the envelope was made with op 'send' const sent = await rarebl.request('POST', '/open/tx/send', { chain: 'btc', from: [from], to: [{ address: to, amount: '0.0015' }], priority: 'standard', keys: { encrypted }, }, hdr); // tx/sign: the envelope was made with op 'sign' const signedOnly = await rarebl.request('POST', '/open/tx/sign', { unsigned: reply.unsigned, keys: { encrypted } }, hdr); ``` ```python Python hdr = {"X-Rarebl-Accept-Risk": "hosted-signing"} # tx/send: the envelope was made with op "send" sent = c.post("/open/tx/send", { "chain": "btc", "from": [from_addr], "to": [{"address": to, "amount": "0.0015"}], "priority": "standard", "keys": {"encrypted": encrypted}, }, headers=hdr) # tx/sign: the envelope was made with op "sign" signed_only = c.post("/open/tx/sign", {"unsigned": reply["unsigned"], "keys": {"encrypted": encrypted}}, headers=hdr) ``` ```php PHP $hdr = ['X-Rarebl-Accept-Risk' => 'hosted-signing']; // tx/send: the envelope was made with op 'send' $sent = $c->post('/open/tx/send', [ 'chain' => 'btc', 'from' => [$from], 'to' => [['address' => $to, 'amount' => '0.0015']], 'priority' => 'standard', 'keys' => ['encrypted' => $encrypted], ], $hdr); // tx/sign: the envelope was made with op 'sign' $signedOnly = $c->post('/open/tx/sign', ['unsigned' => $reply['unsigned'], 'keys' => ['encrypted' => $encrypted]], $hdr); ``` **You should see** `signed` and, for `tx/send`, the `broadcast` result. `tx/sign` returns only `signed`: broadcast it with [Broadcast](https://docs.rarebl.com/api/tx/broadcast). ```json { "signed": { "chain": "btc", "hex": "…", "txid": "…" }, "broadcast": { "txid": "…", "accepted": true, "alreadyKnown": false } } ``` An envelope opens once. If a request fails after the signer read it (a refusal or a timeout), make a new envelope for the retry: resending the same one answers `envelope_replayed`. ## Step 5 — Generate a key (optional) [address/new](https://docs.rarebl.com/api/address/new) generates a keypair in the signer and returns it once. Pass `encryptTo` (your own X25519 public key, base64url) to get the private key back as an envelope encrypted to you (`encryptedPrivateKey`); open it with `envelope.decrypt` (`Envelope::decrypt` in PHP). Generating keys locally with the SDK (`keys.generate`) avoids the round trip. ## How it is protected These rules are enforced by the API: | Rule | What it means | | --- | --- | | Risk header | `X-Rarebl-Accept-Risk: hosted-signing` on every request, otherwise `API403` `risk_not_acknowledged`. | | Scoped envelope | An envelope is bound to its chain and operation, expires within 300 seconds and opens once. `sign` signs only the digests it names; `send` pays only the recipients and amounts it names, and any other output must return to an input address. | | Body only | Keys are accepted in a `POST` body only. A key-like value in the query string gives `API400` `plaintext_key_in_query` and the request is dropped unprocessed. | | Key matching | Every key must derive the address of an input it signs (`key_mismatch`); unused keys are refused. | | No retention | A separate signer process (unix socket, no network) uses the key in memory and wipes it. Keys are never written to disk, logs, the database, caches or error reports, and request bodies on these paths are excluded from all logging. | | Key rotation | The signer key rotates; a retired key is refused 7 days after its `expiresAt`. | | Limits | The write rate class (6 per minute, burst 3, 300 per day) and at most 10 inputs per request. | | Replies | Always `Cache-Control: no-store`. | Only the txid, chain, time and a hashed IP of a broadcast are recorded. ## Common errors | Reply | What it means | What to do | | --- | --- | --- | | `API403` `risk_not_acknowledged` | The header is missing. | Add `X-Rarebl-Accept-Risk: hosted-signing`. | | `API403` `key_mismatch` | A key does not derive the address of an input it signs. | Send only the keys of the input addresses. | | `API403` `digest_not_bound` | `tx/sign`: the digests of the `unsigned` transaction are not exactly the ones in the envelope. Nothing was signed. | List every `toSign` digest of this build, and only those, in `digests`. | | `API403` `outputs_not_bound` | `tx/send`: the transaction does not pay exactly the recipients and amounts in `bind` (or has an output that does not return to an input address). Nothing was signed. | Make `bind` match the `to` of the request, in base units; use `envelope.bindOf`. | | `API400` `bad_envelope` | The envelope is unreadable, is a v1 envelope, names an unknown `kid`, has an `op` that does not match the route, or an `exp` more than 300 seconds ahead. | Fetch the signer key again and encrypt again with SDK 0.2.0 or later. | | `API400` `envelope_expired` | The envelope's `exp` has passed. | Make a new envelope right before the request. | | `API409` `envelope_replayed` | The envelope was already used. | Make a new envelope for every request. | | `API400` `chain_mismatch` | The envelope's `chain` is not the chain of the request. | Use the same chain in the envelope and the request. | | `API400` `plaintext_key_in_query` | A key-like value was in the URL. | Move it into the POST body (as an envelope). | | `API400` with `param: "keys"` | A `keys` member is malformed. | Send `{"encrypted": "…"}` or `{"privateKeys": [...]}`. | | `API422` `tx_too_large` | More than 10 inputs. | Split the spend. | | `API502` `signer_unavailable` | The signer is not reachable. | Retry with back-off, with a new envelope. | | `API429` | Write rate limit. | Wait `Retry-After` seconds. | When a request answers `bad_envelope` or an unknown `kid`, fetch [Signer key](https://docs.rarebl.com/api/tx/signer-key) again before you retry: the key may have rotated. Build and broadcast errors (`insufficient_funds`, `node_rejected` and others) are the same as in [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction). ## Next steps - The local alternative: [Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally) and [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction). - Endpoint details: [tx/sign](https://docs.rarebl.com/api/tx/sign), [tx/send](https://docs.rarebl.com/api/tx/send), [address/new](https://docs.rarebl.com/api/address/new), [Signer key](https://docs.rarebl.com/api/tx/signer-key). --- # Errors and rate limits Production code needs to know which failures to retry, which to fix and how fast it may call. You will read the error envelope, retry the right things with back-off, make writes safe to repeat and stay under the per-IP rate limits. The reference tables are in [Responses & codes](https://docs.rarebl.com/responses-and-codes) and [Rate limits](https://docs.rarebl.com/rate-limits). ## Before you start - Every reply, success or error, has the same envelope `{status, code, message, data, go, meta}` and the HTTP status matches the code. - Branch on `code` and `data.reason`, never on `message`: messages can change. - Every reply carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-Request-Id`. Log the request id (`meta.requestid`, the same value): support will ask for it. ## Step 1 — Read the error An error has `status: 0`, an `API*` code and often a `data` object with the detail. ```json { "status": 0, "code": "API422", "message": "The node rejected the transaction.", "data": { "reason": "node_rejected", "detail": "min relay fee not met" }, "go": null, "meta": { "requestid": "0123456789abcdef", "notices": [] } } ``` ```js JavaScript const res = await fetch(url, options); const body = await res.json(); if (body.code !== 'API200') { const { reason, param, detail } = body.data ?? {}; console.warn(body.code, reason ?? param, detail, body.meta?.requestid); } ``` ```python Python try: c.broadcast("btc", hex_tx) except rarebl.ApiError as e: print(e.code, e.reason, e.detail) # for example API422 node_rejected "min relay fee not met" ``` ```php PHP try { $c->broadcast('btc', $hex); } catch (Rarebl\ApiException $e) { error_log($e->apiCode . ' ' . $e->reason . ' ' . $e->detail); } ``` **You should see** the code, the reason and the node's detail. The SDKs raise typed errors (`ValidationError`, `ConflictError`, `UnprocessableError`, `RateLimitError` and others) that carry the same fields. ## Step 2 — Decide: retry, wait or fix | Code | Meaning | What to do | | --- | --- | --- | | `API429` | Rate limited. | **Wait** `Retry-After` seconds, then retry. | | `API502` | A node is unavailable, syncing or timed out. | **Retry** with back-off. | | `API500` | Internal error. | Retry once or twice; report it with the request id. | | `API409` | Not available yet on this chain, or a precondition is missing (`data.reason` says which). | Not a failure of your request. Start the watch on `not_watched`; otherwise try later. | | `API422` | Well-formed, but the node or our checks reject it. | **Change the transaction** (fund it, raise the fee, rebuild). Do not retry it unchanged. | | `API400`, `API403`, `API404`, `API405` | A bad parameter, a policy refusal, a missing object or a wrong method. | **Fix the request.** Do not retry unchanged. | ## Step 3 — Retry with back-off Retry only what can succeed later: `API429` and `API502` (and `API500` briefly). Honour `Retry-After`, double the delay up to a ceiling and add jitter. ```js JavaScript async function call(url, options, tries = 4) { let delay = 1000; for (let i = 0; i < tries; i++) { const res = await fetch(url, options); const body = await res.json(); if (body.code === 'API200') return body.data; if (body.code === 'API429') delay = Number(res.headers.get('Retry-After') ?? 1) * 1000; else if (body.code !== 'API502' && body.code !== 'API500') throw Object.assign(new Error(body.message), { code: body.code, data: body.data }); await new Promise((r) => setTimeout(r, delay + Math.random() * 250)); delay = Math.min(delay * 2, 30000); } throw new Error('gave up'); } ``` ```python Python import random, time def call(fn, tries=4): delay = 1.0 for _ in range(tries): try: return fn() except rarebl.ApiError as e: if e.code not in ("API429", "API502", "API500"): raise # fix the request or the transaction time.sleep(delay + random.random() * 0.25) delay = min(delay * 2, 30) raise RuntimeError("gave up") ``` **You should see** transient failures absorbed and permanent ones surfaced at once. ## Step 4 — Make writes safe to repeat A retried broadcast must never pay twice. Send an `Idempotency-Key` on [Broadcast](https://docs.rarebl.com/api/tx/broadcast): the same key, or the same txid, within 24 hours returns the first result. Use one stable key per payout (for example your payout id), and queue payouts instead of firing them in a burst: the write class allows 6 per minute (burst 3) and 300 per day. ```bash cURL curl -X POST https://api.rarebl.com/open/tx/broadcast -H "Content-Type: application/json" \ -H "Idempotency-Key: payout-7781" -d '{"chain":"btc","hex":"0200000001…"}' ``` **You should see** `alreadyKnown: true` (or the same txid) if you send it again. ## Step 5 — Stay under the rate limits Limits are per IP address, by rate class; each class is its own bucket, so a burst of builds does not use up your reads. | Class | Endpoints | Limit | | --- | --- | --- | | read | v1 reads, UTXOs, nonce, resources, outspends, market, `hooks/get`, `hooks/deliveries` | 5 per second, burst 20 | | compute | decode, estimate, build, assemble (without broadcast), address validate | 2 per second, burst 10 | | write | broadcast, assemble with `broadcast: true`, hosted sign, send and new address | 6 per minute, burst 3; 300 per day | | watch | address watch | 30 per minute; 2,000 addresses per day | | hooks | create, update, pause, resume, delete, test, replay | 10 per minute; 100 active subscriptions per IP | | stream | `wss://api.rarebl.com/ws` | 3 connections; 200 channels per connection | - Prefer [webhooks](https://docs.rarebl.com/guides/webhooks) or the [WebSocket](https://docs.rarebl.com/guides/websocket) to polling, cache what does not change, and poll no faster than the data changes ([Data freshness](https://docs.rarebl.com/data-freshness)). - Many users behind one address (an office, a carrier, your own backend) share one bucket. Run heavy work from your own server. ## Common errors | Reason | What it means | What to do | | --- | --- | --- | | `API429` | A bucket is empty. | Wait `Retry-After`; slow down or cache. | | `API409` `not_watched` | BTC, LTC or BCH address not watched and active. | [Watch it](https://docs.rarebl.com/guides/watch-addresses). | | `API409` `tx_index_pending`, `history_unavailable`, `before_history_start`, `no_mempool_view` | Data that is not available on this chain right now. | See [Coverage and limits](https://docs.rarebl.com/limits-today). | | `API422` `insufficient_funds`, `dust_output`, `fee_too_low`, `fee_too_high` | The transaction cannot be built or relayed as asked. | Fund it, change the amount or the fee. | | `API422` `inputs_spent`, `inputs_unknown` | An input is spent or unknown. | Re-list the UTXOs and build again. | | `API422` `bad_signature`, `nonce_too_low`, `chain_id_mismatch`, `tx_expired`, `tx_too_large` | A signing or chain-rule problem. | Rebuild and sign again. | | `API422` `node_rejected` | The node refused it; `data.detail` has its message. | Read `detail`. | | `API403` `risk_not_acknowledged`, `key_mismatch` | Hosted signing: the header is missing, or a key does not match. | See [Hosted signing](https://docs.rarebl.com/guides/hosted-signing). | | `API502` | A node is unavailable. | Retry with back-off. | ## Next steps - [Responses & codes](https://docs.rarebl.com/responses-and-codes) and [Rate limits](https://docs.rarebl.com/rate-limits): the full tables. - [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction): the errors specific to building and broadcasting. - [Coverage and limits](https://docs.rarebl.com/limits-today): what is not available on which chain. --- # Everything a platform needs This is what a platform like Rarebl needs from a chain API (deposits, sweeps, payouts, settlement, rates) and where the Open API provides it. Every row links to the endpoints and guides involved. | Need | Where it is in the API | | --- | --- | | Create deposit addresses | The SDK generates keys locally and derives addresses (`keys.generate`, recommended): [Sign locally with the SDK](https://docs.rarebl.com/guides/sign-locally). Or [New address (hosted)](https://docs.rarebl.com/api/address/new): [Hosted signing](https://docs.rarebl.com/guides/hosted-signing). | | Validate an address | [Validate an address](https://docs.rarebl.com/api/address/validate). | | Detect deposits | [Webhooks](https://docs.rarebl.com/guides/webhooks) of type `address.activity` ([Create a webhook](https://docs.rarebl.com/api/hooks/create)); the WebSocket `address:*` channels ([Live data over WebSocket](https://docs.rarebl.com/guides/websocket)); [Address transactions](https://docs.rarebl.com/api/address/transactions) (DASH now, watched addresses on BTC, LTC and BCH from their watch time, ETH and TRON as the explorer index lands). | | Verify deposits and confirmations | [Transaction](https://docs.rarebl.com/api/tx/get) and [Transaction status](https://docs.rarebl.com/api/tx/status); `tx.confirmations` webhooks: [Track a deposit until it confirms](https://docs.rarebl.com/guides/track-a-deposit). | | UTXOs and balances for sweeps and payouts | [Watch addresses](https://docs.rarebl.com/api/address/watch) then [Address UTXOs](https://docs.rarebl.com/api/address/utxos) (BTC, LTC, BCH); DASH natively; ETH and TRON through [Address](https://docs.rarebl.com/api/address/balance) and [Address tokens](https://docs.rarebl.com/api/address/tokens): [Watch addresses, get UTXOs and balances](https://docs.rarebl.com/guides/watch-addresses). | | Fees | [Recommended fees](https://docs.rarebl.com/api/fees/recommended) and [Estimate](https://docs.rarebl.com/api/tx/estimate): [Market data and fees](https://docs.rarebl.com/guides/market-data). | | Build, sign and broadcast (sweeps, payouts, gas top-ups, token transfers) | [Build](https://docs.rarebl.com/api/tx/build), [Assemble](https://docs.rarebl.com/api/tx/assemble), [Broadcast](https://docs.rarebl.com/api/tx/broadcast); SDK local signing; hosted [Sign](https://docs.rarebl.com/api/tx/sign) and [Send](https://docs.rarebl.com/api/tx/send): [Send a transaction](https://docs.rarebl.com/guides/send-a-transaction), [Sign locally with the SDKs](https://docs.rarebl.com/guides/sign-locally) (including ERC-20 and TRC-20 tokens). | | Nonce and resources | [Address nonce](https://docs.rarebl.com/api/address/nonce) (ETH) and [Address resources](https://docs.rarebl.com/api/address/resources) (TRON). | | Stuck payments (RBF and CPFP) | [Build](https://docs.rarebl.com/api/tx/build) with `rbf`, or exact `inputs` spending your own change at a higher `feeRate`: [Stuck transactions](https://docs.rarebl.com/guides/stuck-transactions). | | Spent checks before spending | [Output spend status](https://docs.rarebl.com/api/tx/outspends). | | Settlement (dropped, reverted, replaced) | [Transaction status](https://docs.rarebl.com/api/tx/status); `tx.dropped` and `tx.replaced` webhooks; failed ETH transactions show `status: "failed"`: [Webhooks](https://docs.rarebl.com/guides/webhooks). | | Rates | [Prices](https://docs.rarebl.com/api/market/price) and [FX](https://docs.rarebl.com/api/market/fx): [Market data and fees](https://docs.rarebl.com/guides/market-data). | ## Where coverage differs by chain Address history on BTC, LTC, BCH, ETH and TRON, the Ethereum node (still syncing), TRON history (from 2026-09-08) and the time a watch bootstrap takes vary by chain. See [Coverage and limits](https://docs.rarebl.com/limits-today) for the current state. ## Read the rest - [Errors and rate limits](https://docs.rarebl.com/guides/handle-errors) and [Rate limits](https://docs.rarebl.com/rate-limits): the per-IP classes (read, compute, write, watch, hooks, stream). - [Responses & codes](https://docs.rarebl.com/responses-and-codes): every code and reason, including `API422` and `API403`. - [SDKs](https://docs.rarebl.com/sdks/js): [JavaScript](https://docs.rarebl.com/sdks/js), [Python](https://docs.rarebl.com/sdks/python), [PHP](https://docs.rarebl.com/sdks/php), [shell](https://docs.rarebl.com/sdks/shell). - [API reference](https://docs.rarebl.com/api): every endpoint with Try It. --- # JavaScript SDK The JavaScript SDK is an ES module for **Node 18 or later and browsers**, shipped as a single file, `rarebl.mjs`. It builds, verifies and signs transactions **on your machine**; your private key never leaves the process. Version 0.2.0. It is the reference implementation: the Python and PHP SDKs must produce byte-identical ECDSA signatures (and Schnorr signatures when `deterministic` is set). Crypto comes from the pinned libraries `@noble/curves`, `@noble/hashes` and `@scure/base`, bundled into the single file, so you do not install anything. ## Download | File | Size | SHA-256 | | --- | --- | --- | | [rarebl-sdk-0.2.0.tgz](https://docs.rarebl.com/sdk/js/rarebl-sdk-0.2.0.tgz) | 103.7 KB | `78c11fb5a1c378f55e240556c472b837ed0fbef35b940c63d167ed846e43f5fa` | | [rarebl.mjs](https://docs.rarebl.com/sdk/js/rarebl.mjs) | 197.7 KB | `7c5bd8fc5e412ae8596cedab97634915fd94d91bf2fb8b863c1656e36b070aea` | Requires: Node 18 or later, or a browser (ESM). All checksums are in [SHA256SUMS](https://docs.rarebl.com/sdk/SHA256SUMS). Check the file before you use it: ~~~bash curl -O https://docs.rarebl.com/sdk/js/rarebl.mjs sha256sum rarebl.mjs ~~~ ## Install Download the file, check it against the SHA-256 above, and import it: ~~~js import { Rarebl, keys } from './rarebl.mjs'; ~~~ A tarball with the source and tests (`rarebl-sdk-0.2.0.tgz`) is next to it. The npm package `@rarebl/sdk` will follow. In a browser use `