Send a transaction
Step by step: build, verify, sign the digests locally, assemble and broadcast, with idempotency keys and fee caps.
# 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
<?php
require 'Rarebl.php';
$c = new Rarebl\Rarebl(); // https://api.rarebl.com
$reply = $c->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=<txid>"
```
**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).