Skip to content

Limits and errors

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.

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.

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.

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.

A confirmation is not instantly visible everywhere. After a block that touches your coins or units:

  1. Counterparty has to see the block and parse it (usually well under a minute).
  2. The marketplace’s indexer reads it on its next run; it runs every 2 minutes.
  3. Public reads (books, collections, activity) are edge-cached for up to 30 seconds, and /inventory/attachable for 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/prepared are 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.

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.