Submit a transfer
POST/api/market/transfer/submit
Step 2: the PSBT from prepare with every input signed. The server checks that it is exactly the prepared transaction, verifies every signature, rechecks every UTXO against bitcoind, the rare-sat index and the ord indexer (changed, spent), runs the invariants with the real size and testmempoolaccept, cancels the active listings of the moved UTXOs (their signatures are deleted, so they can never sell) and broadcasts. Idempotent: the same body again answers the same txid.
Request body #
content-type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | From prepare. |
psbt | string | yes | The PSBT from prepare with every input signed, base64 (finalized or not). |
Example request #
curl -X POST 'https://raresats.net/api/market/transfer/submit' \
-H 'content-type: application/json' \
-d '{
"id": "6baec47455bf8df8",
"psbt": "<the PSBT from prepare, every input signed, base64>"
}'const response = await fetch('https://raresats.net/api/market/transfer/submit', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
"id": "6baec47455bf8df8",
"psbt": "<the PSBT from prepare, every input signed, base64>"
})
});
const data = await response.json();import requests
data = requests.post("https://raresats.net/api/market/transfer/submit", json={
"id": "6baec47455bf8df8",
"psbt": "<the PSBT from prepare, every input signed, base64>"
}).json()Response 200 #
| Field | Type | Description |
|---|---|---|
txid | string | The broadcast transaction. |
transfer | object | A wallet transfer: what is on chain or was chosen by its owner. |
transfer.id | string | The prepared transfer's id. |
transfer.txid | string | Its transaction id (what /tx/{txid} shows). |
transfer.kind | string | One of: extract, send, send_n, chop, consolidate, multisend, speedup, cancel. |
transfer.status | string | pending in the mempool; confirmed in a block; replaced by a speed-up or cancel built here (replaced_by); conflicted: a block spent its inputs and it was provably not mined (its own outputs never appeared within six blocks); dropped: in no mempool and its inputs unspent (it may still resurface until voided); unknown: the broadcast's outcome is not known yet; rejected: bitcoind refused it, so it was never sent and cannot resurface. |
transfer.created_at | integer | Unix seconds. |
transfer.updated_at | integer | Unix seconds. |
transfer.owner | object | The owner's addresses (one transfer's read). |
transfer.owner.payment_address | string | Paid the fees. |
transfer.owner.ordinals_address | string | The ordinals address. |
transfer.owner_role | string | The queried address's part in it (a list read). One of: payment, ordinals, both. |
transfer.destination | string | null | Where the moved sats went (null for a multisend). |
transfer.fee_rate | number | sat/vB. |
transfer.network_fee | integer | Sats. |
transfer.tools_fee | integer | The flat raresats.net fee it paid, in sats. |
transfer.vsize | integer | Its size in vbytes. |
transfer.replaces | string | null | The transfer it replaced (speed-up, cancel). |
transfer.replaced_by | string | null | The transaction that replaced it. |
transfer.block_height | integer | null | The block it confirmed in. |
transfer.outputs | array of object | Its outputs. |
transfer.outputs[].index | integer | vout. |
transfer.outputs[].address | string | null | Who it pays. |
transfer.outputs[].value | integer | Sats. |
transfer.outputs[].role | string | dest, return, plain_return, tools_fee or change. |
transfer.actions | object | What its owner can do now. |
transfer.actions.speedup | boolean | It is pending: a speed-up can replace it. |
transfer.actions.cancel | boolean | It could still confirm (pending, unknown, dropped) and its chain of replacements pays someone else: a cancel (or a void) sends its sources back. It never hides a cancel the prepare would allow; the prepare decides with the mempool (409 nothing_to_cancel). |
{
"txid": "a1edadb9e3b45097d272621ea8adcbee697281bd4d611462db134292d4e1cf9c",
"transfer": {
"id": "6baec47455bf8df8",
"txid": "a1edadb9e3b45097d272621ea8adcbee697281bd4d611462db134292d4e1cf9c",
"kind": "extract",
"status": "pending",
"created_at": 1791428236,
"updated_at": 1791428236,
"owner": {
"payment_address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
"ordinals_address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr"
},
"destination": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"fee_rate": 3,
"network_fee": 1059,
"tools_fee": 1000,
"vsize": 352,
"replaces": null,
"replaced_by": null,
"block_height": null,
"outputs": [
{
"index": 0,
"address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"value": 388,
"role": "return"
},
{
"index": 1,
"address": "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
"value": 330,
"role": "dest"
},
{
"index": 2,
"address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
"value": 9282,
"role": "plain_return"
},
{
"index": 3,
"address": "bc1p4hefchfnn06cn2xmk6fq6xcw5ew260q2dptc4c35fcazgedmzlcqkxf057",
"value": 1000,
"role": "tools_fee"
},
{
"index": 4,
"address": "37VucYSaXLCAsxYyAPfbSi9eh4iEcbShgf",
"value": 22941,
"role": "change"
}
],
"actions": {"speedup": true, "cancel": false}
}
}Errors #
{error, code}; see Errors for every code.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_id | id is not a prepared purchase or transfer id. |
| 400 | bad_json | The body is not valid JSON. |
| 400 | bad_psbt | Not a base64 PSBT, or one that does not parse. |
| 400 | bad_request | The body is not a JSON object, or the engine refused a malformed request (e.g. a purchase submitted without its signed setup PSBT). |
| 400, 422 | bad_signature | 400: signature is missing or too long. 422: a signature does not verify (cancel or registration message, listing or purchase input). |
| 403 | cross_origin | A POST from a browser on another site (a foreign Origin, or sec-fetch-site: cross-site). Server-side clients send no Origin and are not affected. |
| 404 | unknown_transfer | No such transfer: an unknown or forgotten prepared id, or a txid the wallet tools did not build. |
| 405 | method_not_allowed | Wrong method for the route; the allow header lists the right ones. |
| 409 | changed | The UTXO's value is not what the PSBT says, or (wallet tools) a UTXO, its sats or the transaction being replaced changed since prepare: prepare again. |
| 409 | config_changed | The marketplace settings changed since this purchase or transfer was prepared (prepare it again), or since this offer was made (it is now invalid, reason config_changed). |
| 409 | conflict | A listing or one of your UTXOs was spent meanwhile (lost race), or bitcoind would not replace a transaction already spending it (insufficient fee), or a transfer's inputs are spent by another transaction. |
| 409 | expired | The prepared purchase, offer or transfer expired, or the offer itself has: prepare it again. |
| 409 | has_descendants | A transaction spends the transfer's outputs already: it can no longer be replaced. |
| 409 | not_pending | The transfer to speed up or cancel is not waiting in the mempool any more (confirmed, replaced). |
| 409 | offer_overlap | Since the prepare, a planned input started funding one of your offers, or a UTXO the server picked got an offer: prepare the transfer again. |
| 409 | pending | The UTXO's listing is being bought, or an offer of it accepted, right now. |
| 409 | spent | The UTXO (or a listing's UTXO) is spent or does not exist. |
| 413 | too_large | The body is over 256 KiB. |
| 415 | not_json | A POST without content-type: application/json. |
| 422 | fee_rate_too_low | fee_rate is below the current minimum (GET /api/market/fees). |
| 422 | fee_too_high | The network fee would exceed the per-transaction cap (200,000 sats): lower the fee rate or move fewer. |
| 422 | missing_signature | An input that must be signed is not. |
| 422 | psbt_mismatch | The signed PSBT is not the prepared transaction. |
| 422 | rejected | bitcoind refused the transaction (reason). |
| 422 | replacement_fee_too_low | The replacement pays too little to replace the pending transaction (BIP 125): raise the fee rate to at least min_fee_rate. |
| 422 | unsupported_script | The trade engine met a script type it does not support. |
| 429 | rate_limited | Over 30 POSTs a minute from your IPv4 address or IPv6 /64 (in chat, counted per IPv4 address or IPv6 /64: 30 writes and 20 login nonces a minute, 20 sign-ins and 10 display-name changes an hour; one message every 3 s and 20 a minute per address); wait retry-after seconds. With the AI moderation on, at most 6 messages and names a minute are checked per address and per IPv4 address or IPv6 /48. |
| 500 | internal | A server bug. Report it. |
| 500 | invariant_violated | A planned or finalized purchase or transfer broke an invariant (violations); nothing was broadcast. |
| 503 | bitcoind_unavailable | bitcoind is not available. |
| 503 | broadcast_unknown | The broadcast's outcome is unknown: submit the same purchase or transfer again to settle it. Carries txid. |
| 503 | engine_unavailable | The trade engine is not available. |
| 503 | fork_behind | The rare-sat index has not caught up with bitcoind: try again in a minute. |
| 503 | fork_unavailable | The rare-sat index is not available. |
| 503 | market_unavailable | The marketplace is closed right now (its backends disagree on the network, or are unreachable). |
| 503 | oracle_unavailable | The ord oracle is not available. |
{"error": "no such transfer", "code": "unknown_transfer"}Submitting is idempotent: the same {id, psbt} again answers the same txid. On 503 broadcast_unknown (or a lost answer), submit the same body again until it answers {txid} or a definite refusal: when bitcoind still does not have the transaction, the server checks it again and sends the same transaction once more (same txid, so nothing is sent twice). Once the transfer has settled, a resubmit answers definitely: {txid} when it confirmed, 409 conflict (with txid, status and replaced_by) when it was replaced or its inputs were spent by another transaction. Offers are checked again just before the broadcast: a planned input that started funding one of your offers since the prepare (unless you chose it knowing, after the spends_offer_funding warning), or a UTXO the server picked that got an offer, answers 409 offer_overlap: prepare again. Your listings on the moved UTXOs are cancelled just before the broadcast, so they can never sell after you signed a move of them.
Follow the transfer with Get a wallet transfer; speed it up or cancel it with Speed up or cancel a transfer.