# Submit a purchase

`POST /api/market/purchase/submit`

Step 3 of buying: the PSBTs from prepare, your inputs signed. The server checks they are exactly the prepared transactions, verifies your signatures, adds the sellers' signatures, rechecks every rule, then `testmempoolaccept` and broadcasts (setup first). The listings become `pending`. Resubmitting the same purchase is idempotent: use it to settle `broadcast_unknown`.

#### Request body

`content-type: application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | The prepared purchase id. |
| `setup_psbt` | string \| null | no | The signed setup PSBT, base64: required when prepare returned `setup`, absent otherwise. |
| `purchase_psbt` | string | yes | The signed purchase PSBT, base64. Not finalized is fine; the transaction must be exactly the prepared one. |

#### Example request

```bash title="cURL"
curl -X POST 'https://raresats.net/api/market/purchase/submit' \
  -H 'content-type: application/json' \
  -d '{
  "id": "5c9e5c410688cd85",
  "purchase_psbt": "cHNidP8BAP3lAQIAAAAEV/MhdPD/0sIHo8hX35p9GQud7geCW9jXa3hoAejEjgoAAAAAAP3///+3Sw6TNUSithUu7/zVH0UQDemfGc8HWcjxkuB/9nd1LAAAAAAA/f///6nTU92uYg6gtb+FEVnT5JxNFJ1H13OjJCln4+xg+7YvAAAAAAD9////VHDf42dMx3y1IrYZcBnC/YiD1iNsIqK5yRDtpRMrGUYAAAAAAP3///8JsAQAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4ciAgAAAAAAACJRIKYIafDbzx3GWcnOy6+AUBNeqejNxIcFPx3GiAlJ3GhMguwAAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4vUCAAAAAAAAIlEgrfKcXTOb9Ymo27aSDRsOplytPApoV4riNE46JGW7F/D1AgAAAAAAACJRIKgvKZRNZbhq5rXlzHXilOrWxZORoe3F4BbjSYxn/Hu7WAIAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4dYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abh3hIAQAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHAAAAAAAAAAAOagxyYXJlc2F0cy5uZXQAAAAAAAEBIFgCAAAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHIgIDmztpS4/FteB/sGnHg8rHVPXTjD4IvtGWDjH9sd2jXCRIMEUCIQDqT5JFB8nk2LvYxjpKXk87G12Tn0Jt7fslQf2gSIvJjwIgbg+BAy20UrXd2niDIeZbPLkgyz+NcnSSkChVICw0gV4BAQQWABT5kGeayv4lwnYVNztAvyJEbST/RAABASBYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abhyICA5s7aUuPxbXgf7Bpx4PKx1T104w+CL7Rlg4x/bHdo1wkRzBEAiAbu6SfmqbPok5pJyyLQoLTWtu6xHYFUxdkVqMdge1hPwIgXVp1PF1DvIaRLu2QeT0Qv9t+5Dfe2BUaY/Y2QBv58+ABAQQWABT5kGeayv4lwnYVNztAvyJEbST/RAABAR8iAgAAAAAAABYAFMDOvNbD08qMddxexi6+VTMO+RDiAAEBIPBJAgAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHIgIDmztpS4/FteB/sGnHg8rHVPXTjD4IvtGWDjH9sd2jXCRHMEQCIC8ykJLr2bz7gpup8hfRujOnTV4UdPVuIsfzFgJGmMuCAiARSDj1rK63S1wPEKMrRPqnZ7u+rJDysVPRn7ym3iNFpQEBBBYAFPmQZ5rK/iXCdhU3O0C/IkRtJP9EAAAAAAAAAAAAAA=="
}'
```
```js title="JavaScript"
const response = await fetch('https://raresats.net/api/market/purchase/submit', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    "id": "5c9e5c410688cd85",
    "purchase_psbt": "cHNidP8BAP3lAQIAAAAEV/MhdPD/0sIHo8hX35p9GQud7geCW9jXa3hoAejEjgoAAAAAAP3///+3Sw6TNUSithUu7/zVH0UQDemfGc8HWcjxkuB/9nd1LAAAAAAA/f///6nTU92uYg6gtb+FEVnT5JxNFJ1H13OjJCln4+xg+7YvAAAAAAD9////VHDf42dMx3y1IrYZcBnC/YiD1iNsIqK5yRDtpRMrGUYAAAAAAP3///8JsAQAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4ciAgAAAAAAACJRIKYIafDbzx3GWcnOy6+AUBNeqejNxIcFPx3GiAlJ3GhMguwAAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4vUCAAAAAAAAIlEgrfKcXTOb9Ymo27aSDRsOplytPApoV4riNE46JGW7F/D1AgAAAAAAACJRIKgvKZRNZbhq5rXlzHXilOrWxZORoe3F4BbjSYxn/Hu7WAIAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4dYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abh3hIAQAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHAAAAAAAAAAAOagxyYXJlc2F0cy5uZXQAAAAAAAEBIFgCAAAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHIgIDmztpS4/FteB/sGnHg8rHVPXTjD4IvtGWDjH9sd2jXCRIMEUCIQDqT5JFB8nk2LvYxjpKXk87G12Tn0Jt7fslQf2gSIvJjwIgbg+BAy20UrXd2niDIeZbPLkgyz+NcnSSkChVICw0gV4BAQQWABT5kGeayv4lwnYVNztAvyJEbST/RAABASBYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abhyICA5s7aUuPxbXgf7Bpx4PKx1T104w+CL7Rlg4x/bHdo1wkRzBEAiAbu6SfmqbPok5pJyyLQoLTWtu6xHYFUxdkVqMdge1hPwIgXVp1PF1DvIaRLu2QeT0Qv9t+5Dfe2BUaY/Y2QBv58+ABAQQWABT5kGeayv4lwnYVNztAvyJEbST/RAABAR8iAgAAAAAAABYAFMDOvNbD08qMddxexi6+VTMO+RDiAAEBIPBJAgAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHIgIDmztpS4/FteB/sGnHg8rHVPXTjD4IvtGWDjH9sd2jXCRHMEQCIC8ykJLr2bz7gpup8hfRujOnTV4UdPVuIsfzFgJGmMuCAiARSDj1rK63S1wPEKMrRPqnZ7u+rJDysVPRn7ym3iNFpQEBBBYAFPmQZ5rK/iXCdhU3O0C/IkRtJP9EAAAAAAAAAAAAAA=="
  })
});
const data = await response.json();
```
```python title="Python"
import requests

data = requests.post("https://raresats.net/api/market/purchase/submit", json={
    "id": "5c9e5c410688cd85",
    "purchase_psbt": "cHNidP8BAP3lAQIAAAAEV/MhdPD/0sIHo8hX35p9GQud7geCW9jXa3hoAejEjgoAAAAAAP3///+3Sw6TNUSithUu7/zVH0UQDemfGc8HWcjxkuB/9nd1LAAAAAAA/f///6nTU92uYg6gtb+FEVnT5JxNFJ1H13OjJCln4+xg+7YvAAAAAAD9////VHDf42dMx3y1IrYZcBnC/YiD1iNsIqK5yRDtpRMrGUYAAAAAAP3///8JsAQAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4ciAgAAAAAAACJRIKYIafDbzx3GWcnOy6+AUBNeqejNxIcFPx3GiAlJ3GhMguwAAAAAAAAWABTAzrzWw9PKjHXcXsYuvlUzDvkQ4vUCAAAAAAAAIlEgrfKcXTOb9Ymo27aSDRsOplytPApoV4riNE46JGW7F/D1AgAAAAAAACJRIKgvKZRNZbhq5rXlzHXilOrWxZORoe3F4BbjSYxn/Hu7WAIAAAAAAAAXqRQ/tulYEuV7tGkfmkpiiGKmGk92m4dYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abh3hIAQAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHAAAAAAAAAAAOagxyYXJlc2F0cy5uZXQAAAAAAAEBIFgCAAAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHIgIDmztpS4/FteB/sGnHg8rHVPXTjD4IvtGWDjH9sd2jXCRIMEUCIQDqT5JFB8nk2LvYxjpKXk87G12Tn0Jt7fslQf2gSIvJjwIgbg+BAy20UrXd2niDIeZbPLkgyz+NcnSSkChVICw0gV4BAQQWABT5kGeayv4lwnYVNztAvyJEbST/RAABASBYAgAAAAAAABepFD+26VgS5Xu0aR+aSmKIYqYaT3abhyICA5s7aUuPxbXgf7Bpx4PKx1T104w+CL7Rlg4x/bHdo1wkRzBEAiAbu6SfmqbPok5pJyyLQoLTWtu6xHYFUxdkVqMdge1hPwIgXVp1PF1DvIaRLu2QeT0Qv9t+5Dfe2BUaY/Y2QBv58+ABAQQWABT5kGeayv4lwnYVNztAvyJEbST/RAABAR8iAgAAAAAAABYAFMDOvNbD08qMddxexi6+VTMO+RDiAAEBIPBJAgAAAAAAF6kUP7bpWBLle7RpH5pKYohiphpPdpuHIgIDmztpS4/FteB/sGnHg8rHVPXTjD4IvtGWDjH9sd2jXCRHMEQCIC8ykJLr2bz7gpup8hfRujOnTV4UdPVuIsfzFgJGmMuCAiARSDj1rK63S1wPEKMrRPqnZ7u+rJDysVPRn7ym3iNFpQEBBBYAFPmQZ5rK/iXCdhU3O0C/IkRtJP9EAAAAAAAAAAAAAA=="
}).json()
```

#### Response `200`

| Field | Type | Description |
| --- | --- | --- |
| `txid` | string | The purchase transaction, accepted by bitcoind and broadcast. |
| `setup_txid` | string | The setup transaction (broadcast first), when there was one. |

```json title="Response"
{
  "txid": "6897451c42f02de2594abfdd6a5d4fcf4794a62cbbd9fbec1ec983893ddc5df4"
}
```

#### Errors

`{error, code}`; see [Errors](https://raresats.net/docs/errors.md) 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_purchase` | No such prepared purchase (it was dropped: your 11th prepared since, a busy server, or a restart): prepare it again. |
| 405 | `method_not_allowed` | Wrong method for the route; the `allow` header lists the right ones. |
| 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 | `listing_changed` | A listing's stored terms no longer match the plan: prepare it again. |
| 409 | `not_active` | A listing (or offer) is not active: being bought (or accepted), sold, cancelled, moved, expired or invalid. Carries `id` and `status`. |
| 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 | `bad_sighash` | A signature uses the wrong sighash type (listings: 0x83; purchases: DEFAULT or ALL). |
| 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`). |
| 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. |
| 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_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. |

```json title="Error (409)"
{
  "error": "the prepared purchase expired: prepare it again",
  "code": "expired"
}
```

## When the answer is lost

A submit that times out, or answers 503 `broadcast_unknown`, may still have gone through. **Submit the same body again**: it is idempotent. If bitcoind has the transaction, the answer is the same `{txid}`; if it was refused, you get that error; while it cannot be known yet, `broadcast_unknown` again. Meanwhile the listings stay reserved (`pending`) for you, and return to sale if the transaction never reaches the mempool.
