Limits and errors
Error envelope
Section titled “Error envelope”API-generated failures use the same shape, including unknown routes and
unsupported methods. Branch on code; the error text is for humans and may
change. A proxy or Cloudflare rejection before the request reaches the API can
have a different format.
{ "error": "too many requests; retry shortly", "code": "rate_limited" }Some errors add fields (for example retryable: true, minimum_price_sats, or
shortfall_sats). Read the HTTP status and JSON code together. For example,
409 checkout_changed requires a fresh quote and signature; retrying the old
PSBT does not resolve it. Preserve additional response fields for diagnosis
and recovery.
HTTP clients
Section titled “HTTP clients”Use a descriptive User-Agent for server-side integrations. An edge rejection
can return HTML or a non-API error before your request reaches the service, so
check the status and content type before parsing JSON.
Limits
Section titled “Limits”| Boundary | Limit |
|---|---|
| Expensive writes, with a session | Exactly 20 in any 60 seconds per route and session address, then 429 rate_limited |
| Expensive writes, anonymous | Exactly 20 in any 60 seconds per route and IP |
| Expensive writes, per-IP ceiling | Exactly 120 in any 60 seconds per route and IP, across every session |
| Signed completions | Exactly 120 in any 60 seconds per route and session address (IP when anonymous), under a 600 per-IP ceiling |
Sign-in (/v1/auth/*) |
Exactly 60 in any 60 seconds per route and IP |
| Uncached reads | 300 in any 60 seconds per session address (IP when anonymous), with an 1,800 per-IP ceiling; one shared budget across paths and filters |
| Recovery reads | Separate 300/1,800 budget for operation status and recovery inboxes |
| Account maintenance | Separate 120/600 budget shared by delist, offer/placement/authorization cancellation, draft discard/price and cancellation review |
| Artist profile edit/check | 6 per address, 30 per IP in any 60 seconds; shared by both actions |
| JSON request body | 64 KiB on ordinary write routes |
| Checkout cart | 10 lines, 20 units |
| Exact-offer placement | 20 funding slots, 500 targets, 1,000 slot/target combinations |
| Exact authorization batch | 100 asset outpoints |
| Collection offer | 100 alternatives, 8 funding inputs, 25 open funding sets per address |
| Transaction status batch | 25 txids |
| Paged histories | At most 100 rows per page; follow next_cursor |
“Expensive writes” are the routes that compose or verify against the chain before answering: listing and attach preflights, checkout quotes, offer funding and placement, exact authorizations, counters, collection-offer preflight/placement and acceptance preflight, listing batches, reprice preflights, offer-selection validation, and transaction relay. Each route has its own bucket, so one workflow cannot starve another.
Signed submissions have a separate budget from quote creation. Send a current
x-session-token even on routes where it is optional, so the address budget
applies. The per-IP ceiling still applies across all sessions on that IP.
Read budgets are shared across their routes and filters. Public cache hits do not consume read capacity; changing a cursor or filter does not create a new budget. Recovery and cancellation have separate budgets so routine polling does not use their capacity.
On 429 rate_limited, wait for retry_after_seconds or the matching
Retry-After header before trying again. A rejected request does not count
against the window. Do not retry an uncertain write until you have checked its
status.
{ "error": "too many requests; retry shortly", "code": "rate_limited", "retry_after_seconds": 37 }Paged reads return {result, result_count, next_cursor}. Pass cursor back
unchanged; a cursor is valid only with the filters it was issued under.
Error codes
Section titled “Error codes”| Code | Typical status | Meaning |
|---|---|---|
invalid_request |
400, 413 | Malformed or out-of-range input |
unsupported_address_type |
400 | A valid address in a role its family cannot play, usually a legacy address asked to sign. See Authentication |
unauthenticated |
401 | Missing, expired, or wrong-address session |
forbidden |
403 | Authenticated, but not the owner |
not_found |
404 | No such resource (private resources also answer this to non-owners) |
method_not_allowed |
405 | The path exists, but does not support this HTTP method; Allow lists supported methods |
state_changed |
409 | The order or outpoint changed under you |
expired |
410 | The quote, preflight, or counter has expired |
invalid_transaction |
422 | The PSBT or transaction failed verification |
signature_invalid |
401, 422 | A login proof or transaction signature did not verify |
template_mismatch |
422, 409 | The signed PSBT differs from the stored template |
checkout_changed |
409 | The cart can no longer settle as quoted. See Buying |
insufficient_funds |
422 | Asset-free BTC cannot cover the quote; body has need_sats, have_sats, shortfall_sats |
insufficient_asset_balance |
422, 409 | The balance cannot cover an attach or its XCP fee; body has asset, held_raw, committed_raw, needed_raw, purpose |
price_below_minimum |
422 | Below 5,000 sats; body has minimum_price_sats |
listing_price_above_maximum |
422 | Ask above 100 BTC; body has maximum_price_sats. Funded offers have no equivalent ask cap |
counter_price_invalid |
422 | A counter that does not beat the offer in 1,000-sat steps; body has minimum_counter_sats |
self_trade |
409 | The same address would be buyer and seller: checkout skips your own listings (and answers this, with own_listings, when only yours are left), and you cannot offer on or sell into your own offer |
duplicate_funding_slot |
409 | A bulk selection names two offers backed by the same funding slot |
offer_selection_unavailable |
404, 409 | A selected offer can no longer be accepted |
attached_delivery_single_item_only |
422 | Attached delivery was requested for more than one item |
mempool_rejected |
422 | A node refused the transaction on policy that retrying cannot fix; body has mempool_policy (fee_floor: re-quote at a higher fee_rate; chain_too_long or truc: wait for a confirmation) and, on a completion, claims_released |
conflict_spend |
409 | An input was already spent by another transaction |
broadcast_unknown |
502 | The broadcast outcome is uncertain. Reconcile; do not re-sign |
offer_capacity_full |
503 | No collection-offer anchor is free right now; retry shortly |
open_offer_limit |
409 | The address already has 25 open collection-offer funding sets |
policy_offer_refused |
422, 409 | The market signer or protocol verifier refused; body has refusal_code |
rate_limited |
429 | Slow down |
upstream_unavailable |
502 or 503 | Bitcoin or Counterparty backend unavailable, often retryable: true |
service_unavailable |
503 | A required dependency is unavailable |
internal_error |
500 | Unexpected failure |
Statuses are the usual ones; a route can differ, so check its
entry in the API reference. The client adds one code of its own,
invalid_response, when a response is not the expected envelope.
After a block: give it a few minutes
Section titled “After a block: give it a few minutes”A confirmation is not instantly visible everywhere. After a block that touches your coins or units:
- Counterparty has to see the block and parse it (usually well under a minute).
- The marketplace’s indexer reads it on its next run; it runs every 2 minutes.
- Public reads (books, collections, activity) are edge-cached for up to 30
seconds, and
/inventory/attachablefor 10. The cache key holds only the query parameters a route reads, so an extra parameter such as a cache-buster is ignored and does not force a fresh read. Session-owner reads and/inventory/preparedare never cached.
So expect about 2 to 3 minutes between a block and the market reflecting it:
a delivered unit appearing in /inventory/attachable, a new attach appearing in
/inventory/prepared, a listing preflight moving from awaiting_indexer to
ready, a sold or delisted ask leaving the public book. Your own writes answer
at once; what lags is the market’s view of the chain.
If you check immediately and something is missing, that is usually this window,
not a failure. A few refusals look like “you don’t have it” while it lasts, for
example an attach template answering insufficient_asset_balance with
held_raw: "0" right after a unit was delivered to you. Wait a couple of
minutes and retry the balance read; don’t re-sign or re-broadcast anything.
Use the saved transaction or operation status to distinguish a pending
transaction from a balance that has not been indexed yet.
Separate from this window, exact offers need the asset UTXO at 3 confirmations (about 30 minutes), and a zero-fee collection-offer sale can take a few blocks to confirm; see Offers.
Retry guidance
Section titled “Retry guidance”Reads are safe to retry. Writes are not blindly retryable.
| Response | What to do |
|---|---|
401 |
Get a fresh session for the same address, then re-read state before resuming |
409 |
Re-read that order or outpoint. Never silently switch to another asset or funding output |
429 |
Wait retry_after_seconds (the Retry-After header carries the same value), then reconcile before retrying a write |
503 offer_capacity_full |
Retry after a short wait |
422 from /v1/attach-templates with reason: no_funding and retryable: true |
Your BTC exists but is still confirming: retry after the next block, or chain on your previous attach’s change by passing it as funding |
503/502 during completion |
Read the saved operation status first. Request a fresh quote only after confirming the old operation cannot complete |
Timeout or 5xx |
Does not prove a write failed. Reconcile first, using Recovery |
Repeat a write only when it is idempotent: a listing preflight with the same
idempotency_key, an identical POST /v1/policy-offers, or re-submitting the same
signed bytes. Quotes never reserve inventory, so a stale quote fails with 409;
request a new one and sign again rather than retrying old bytes.
Pace requests to stay within each route’s budget. Stop after an uncertain write and reconcile its status before continuing.