How the API fails, what each failure means, and what to do. Every error body is application/problem+json with a stable code. Try things safely in your sandbox from Get started; probe the platform with GET /v1/monitoring/health.
Reading an error · Tokens · Listings · Bids and counters · Deals, escrow and fees · Signals and live streams · Spreadsheet import · Sandboxes and the demo
Every error is application/problem+json: {type, title, status, detail, code, instance}. Branch on code, never on the English in detail. instance is the request id to quote when asking for help.
| Symptom or code | What it means | What to do |
|---|---|---|
| 400 (validation) | Body or query failed the schema; detail lists field: message pairs | Fix the field named. Money is integer cents; enums are lower-case; ids are UUIDs; dates are ISO 8601 with a Z |
401 unauthorized | No token, expired token, token from another environment (staging vs production issuer), or the user's token_version moved (logout everywhere, suspension, sandbox reset) | Mint or refresh a token for this API base. After a sandbox reset every old token is dead by design: mint again |
403 forbidden | Right token, wrong role: a buyer calling a vendor route, a vendor touching another company's listing, a company-admin calling admin routes | Check GET /v1/me: user.role and company.id. The per-role documents list exactly what each role may call |
403 read_only | The token came from a shared, read-only sandbox link | Viewers can look but not change. Open your own sandbox, or ask the owner for an editing session |
404 not_found | Wrong id, or an object you are not allowed to see (private listings, other companies' deals and invoices return 404, not 403, so ids leak nothing) | Search again with your own token; listing ids come from GET /v1/listings, bid ids from GET /v1/bids |
409 concurrent | Someone changed the object between your read and your write (deals carry a version; counters and bids check status) | Re-read and retry once. Never loop on 409 |
412 etag_mismatch / 428 if_match_required | Settings writes need the current ETag | GET /v1/admin/settings, copy the ETag header into If-Match, send the PUT |
413 import_too_large | More rows than listings.import_max_rows | Split the sheet; the limit is in the settings schema |
| 5xx | Rare: a dependency failed. GET /v1/monitoring/health (admin or service) says which | Retry with backoff; the request id in instance finds the log line |
| Symptom or code | What it means | What to do |
|---|---|---|
| Access token stops working after an hour | Access tokens live one hour | Call POST /v1/auth/refresh with the refresh token; you get a new pair. Store the new refresh token: the old one is spent |
| Refresh returns 401 although it is not expired | Refresh tokens are single-use and belong to a family. Reusing a spent one is treated as theft and revokes the whole family | Log in again. Make sure only one process holds a refresh token; two workers sharing one will keep revoking each other |
Everything 401 after all_devices logout, suspension or sandbox reset | These bump the user's token_version; access tokens carry the old value | Log in or mint again. This is the intended kill switch |
| Token works on staging, not production | Tokens carry the issuer of the API that minted them | Each API base has its own tokens; the demo site's tokens are for the sandbox API only |
| Symptom or code | What it means | What to do |
|---|---|---|
floor_on_fixed | A fixed-price listing cannot carry a private floor | Send pricing_mode: negotiable, or drop floor_cents |
floor_above_ask · moq_above_quantity · quantity_below_reserved | Arithmetic rules checked on the merged result of a PATCH, not just the fields you sent | Read the listing first; a PATCH that lowers quantity below what deals already reserve is refused |
used_details_required | Used inventory needs the used block (sale format, functional status, cosmetic grade) | Send it on create; on import set condition to A, B or C and it is filled in |
Published, but status is review | listings.review_required is first_listing by default: a company's first listing waits for the operator | An admin approves with POST /v1/admin/listings/{id}/approve; later listings go straight to active. The ops queue lists what is waiting |
| My listing is not in search | Search shows only active, unexpired listings in the caller's sandbox. Anonymous calls see the shared data unless ?sandbox= names one | Use ?mine=true with your token to see every status; check expires_at |
visible_band_disabled · visible_band_too_wide | Marketplace settings forbid visible bands, or cap them | See negotiation.floor_mode and listings.max_discount_visible_band_pct |
| Symptom or code | What it means | What to do |
|---|---|---|
Bid returns 201 but status: auto_declined | Price under the seller's private floor. The seller is never told; the buyer sees the reason | Bid higher. The floor is never revealed; price history shows where deals closed |
listing_not_active · listing_expired · quantity_unavailable · quantity_below_moq | The listing changed since you read it, or the quantity breaks its rules | Re-read the listing: quantity_available excludes units reserved by open deals; moq is the minimum |
bid_not_open | The bid is already accepted, declined, withdrawn or expired | Read GET /v1/bids/{id}; only open and countered bids take actions |
not_your_turn | Counters alternate: whoever made the last counter must wait | Accept, decline or let it lapse; or wait for the other side |
counter_expired · bid expired | Each bid and counter carries expires_at; the 15-minute sweep expires them and signals both sides | Place a new bid from the listing; expiry does not count as a round |
| Counter refused as out of range | Vendor counters must sit between the buyer's last price and the ask; buyer counters between their last price and the vendor's | The thread shows both bounds; max_rounds on the bid is a snapshot of the setting at bid time |
| Duplicate bids on retry | Retries without Idempotency-Key create duplicates until the per-listing open-bid limit refuses them | Always send Idempotency-Key (a UUID). A replay with the same key and body returns the original; a different body with the same key is 409 |
| 429 or a limit error on bids | bidders.rate_limit_per_hour, bidders.max_open_bids_per_listing and bidders.max_auto_declined_per_listing_per_day are enforced per company | Values are in the settings schema; auto-declined bids count toward the hourly limit |
party_suspended | One party is suspended (strikes, or an operator decision) | Nothing moves until reinstated; the ops queue shows recent suspensions |
| Symptom or code | What it means | What to do |
|---|---|---|
illegal_transition | The state machine is linear: accepted → payment_pending → payment_confirmed → ready_released → shipped_picked_up → received → completed, with disputed and cancelled from most states | detail lists the allowed next statuses. Only the party named by the rules may make each move (buyer confirms receipt, vendor ships) |
escrow_required | Escrow is required by escrow.mode or by escrow.required_above_cents | POST /v1/deals/{id}/escrow first, then payment_pending |
| Deal cancelled by itself, buyer got a strike | Unpaid past reserved_until (deals.reservation_hours); the sweep cancels, releases stock and strikes the buyer; enough strikes suspend | Move to payment_pending inside the hold; signals warn before expiry |
| Disputed and stuck | Only the operator can leave disputed | POST /v1/ops/deals/{id}/resolve with continue or cancel and a note; both parties are signalled |
| No fee entry after completion | Fees accrue at fee.trigger_state (default completed) and only if fee_cents > 0 on the deal | Check the deal's fee_trigger_state and the ledger with ?deal_id=; a 0% fee or fee.min_cents=0 yields nothing |
| Fee invoiced to the wrong party | fee.charged_to at deal time decides (vendor, buyer, split) | The invoice shows fee.payers; the setting is frozen per deal |
| Symptom or code | What it means | What to do |
|---|---|---|
426 on /v1/signals/stream | Not a WebSocket upgrade, or the ticket is missing | POST /v1/signals/stream-token, then open wss://<api>/v1/signals/stream?t=<token> within 60 seconds |
| Stream opens, then nothing | The first frame is {type:'hello', items} with the hot list; new rows arrive as {type:'signals'} | Idle sockets are fine: send the text ping and expect pong. After a reconnect send {"type":"recent"} for the hot page |
{type:'reset'} arrived | The sandbox was reset; the hot list is gone and your token is about to be invalid | Close the socket, mint a new token, reload state |
| Unread count never drops | Unread is everything newer than the seen cursor | POST /v1/signals/seen with no body marks everything, or pass up_to |
| Market feed looks empty or wrong | The market feed is per sandbox; anonymous calls read the shared data unless ?sandbox= | Signed-in callers always get their own sandbox's market |
| The same signal twice | Dedupe keys stop repeats from sweeps and retries; two genuinely different events can look alike | Use id as the identity, never title and body |
| Symptom or code | What it means | What to do |
|---|---|---|
403 import_disabled | listings.spreadsheet_import_enabled is off | An admin turns it on |
Row says import_missing_fields | A row whose mpn matches nothing needs title, category, quantity and ask to create a listing | Rows that match an existing part number only need the fields you want changed |
| Row says duplicate mpn in this sheet | The same part number twice in one upload | One row per part number; quantities are absolute, not additive |
Everything created as fixed | No floor on the row means fixed price | Add a floor column (dollars) or pricing_mode |
| Good rows did not land because of bad ones | They do land: the report is per row | Read report[].action; fix the error rows and re-upload only those (matched by mpn they become updates) |
| Symptom or code | What it means | What to do |
|---|---|---|
| I see different data than a colleague | Every person has their own sandbox; the market, search and feeds are scoped | Share a read-only link, or compare the sandbox_id the token exchange returned |
| Reset wiped my tokens | By design: reset bumps token versions so stale scripts cannot keep writing | Mint again after a reset |
| Postman collection has empty variables | The collection is generated before a token exists | Mint first, then download; or fill access_token and base_url by hand |
| CORS error in a browser app | Only listed origins may call from a browser (CORS_ORIGINS) | Server-side calls are unaffected; ask for your origin to be added |
Mirrored in the API design document, section 12. Request ids (instance) find the log line; include one when reporting a problem to bill@samas.tech.