Errors
RFC 7807 problem documents and the full error catalog for the 2QUIC Marketplace API.
Every error from the 2QUIC Marketplace API is an RFC 7807 problem document served with Content-Type: application/problem+json. Unlike successful responses, error bodies are flat: there is no {data, meta} envelope around them.
The problem document
A real response from POST /v1/leases when the requested epoch range overlaps an existing exclusive lease:
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://docs.swqos.dev/errors/lease-slot-taken",
"title": "Lease slot already booked",
"status": 409,
"detail": "Requested epoch range overlaps an existing lease for this validator.",
"extensions": {
"conflicting_lease_id": "01JXF6Y0Q8ZJ4T2M9R3W5B7DKE"
}
}
| Field | Meaning |
|---|---|
type | Stable URI ending in the error slug. Use the slug as your machine-readable discriminant. Do not dereference the URI, this catalog page is the reference. |
title | Short human summary. Constant for a given error class. |
status | HTTP status code, repeated in the body for non-HTTP consumers. |
detail | Human explanation specific to this occurrence. May vary, do not parse it. |
instance | Optional. The request path this problem refers to. |
extensions | Optional open object carrying domain-specific fields, for example conflicting_lease_id on a booking conflict. |
Branch on the slug at the end of type (for example lease-slot-taken), never on title or detail text. The slug is the stable contract.
Retry guidance at a glance
- 400, 401, 403, 404, 422: your request is wrong or the resource state does not allow the action. Retrying the same request will fail the same way. Fix the request or the workflow first.
- 409: a conflict with concurrent or existing state. Some are retryable (
idempotency-conflict,extension-race,concurrent-rotation), most mean "pick something else". - 429: rate limited. Honor
Retry-Afterand theX-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Resetheaders. Limits are listed on the authentication page. - 5xx: transient server or upstream failure. Retry with exponential backoff. Writes guarded by
Idempotency-Keyare safe to retry with the same key.
Error catalog
400 Bad Request
| Slug | When it fires | Client behaviour |
|---|---|---|
idempotency-key-missing | Idempotency-Key header absent on one of the three guarded POSTs (/v1/leases, /v1/leases/{id}/extend, /v1/validators/{id}/leases/{lease_id}/revoke) | Send a fresh ULID key and retry |
idempotency-key-malformed | The key is not a 26-character Crockford base32 ULID | Generate a proper ULID |
invalid-cursor | Pagination cursor is not a cursor this API issued | Restart from the first page, never fabricate cursors |
invalid-limit | limit outside the allowed range (validators 1 to 50, audit log 1 to 200) | Clamp the value |
invalid-epoch-start | epoch_start exceeds the i32 range. Booking does not reject past epochs, so validate the window client-side before paying | Send a real epoch number |
invalid-epoch-count | epoch_count outside 1 to 30 | Fix the count |
invalid-epoch-range | The requested epoch window fails validation | Fix the window |
invalid-price-bound | price_min_lamports / price_max_lamports filter invalid on GET /v1/validators | Fix the filter |
lease-total-overflow | The lease total (price times epochs) overflows the supported amount | Reduce epoch_count |
invalid-stars | Rating stars outside 1 to 5 | Fix the value |
invalid-comment | Rating comment fails validation | Shorten or clean the comment |
invalid-reply-text | Validator rating reply fails validation | Shorten or clean the reply |
register-timestamp-skew | Registration challenge timestamp outside the allowed skew window (5 minutes) | Re-sign the challenge with a fresh timestamp |
register-invalid-field | A registration field fails basic validation | Fix the field named in detail |
too-many-reports | Usage ingest batch above 1024 reports | Split the batch |
signs-delta-overflow | A usage report signs_delta would overflow the stored counter | Fix the reporting agent, deltas of this size are a bug |
401 Unauthorized
| Slug | When it fires | Client behaviour |
|---|---|---|
auth-required | Missing or invalid bearer credentials on an authenticated route | Authenticate, see authentication |
user-not-provisioned | The session was verified but could not be resolved to a marketplace user | Sign in again through the web app |
register-signature-invalid | The Ed25519 challenge signature on POST /v1/validators or PUT /v1/validators/payout-wallet does not verify | Re-sign with the validator identity key and check the exact challenge byte layout, see validator endpoints |
403 Forbidden
| Slug | When it fires | Client behaviour |
|---|---|---|
forbidden | Authenticated, but you do not own the resource or lack the required role | Do not retry, check which identity you are calling with |
cannot-lease-own-validator | A trader tried to book a lease on a validator they registered themselves | Book a different validator |
lease-not-active | POST /v1/leases/{id}/token on a lease that is not in the active state | If the lease is pending_payment, complete the Solana Pay payment and wait for settlement. If revoked or completed, stop minting tokens |
lease-epoch-out-of-range | A token was requested outside the lease's paid epoch window | Extend the lease or stop, see trader endpoints |
404 Not Found
| Slug | When it fires | Client behaviour |
|---|---|---|
lease-not-found | No lease with that public_id visible to you | Check the ID |
validator-not-found | No validator with that public_id | Check the ID, the validator may have been taken down |
credentials-already-shown | Second call to the one-shot POST /v1/leases/{id}/credentials reveal | The cleartext token is shown exactly once. If you lost it, rotate via POST /v1/leases/{id}/api-keys |
rating-not-found | Reply attempted on a rating that does not exist | Check the rating ID |
onboarding-session-not-found | Unknown or expired onboarding token | Start a new onboarding session from the web funnel |
solana-pay-memo-not-found | POST /v1/solana-pay/{memo} with an unknown, expired, or already-paid memo. The GET on the same path always returns 200 with the label and icon, so it cannot be used to probe memo validity | The pending-payment window is 15 minutes. If it elapsed, book again to get a fresh memo |
409 Conflict
| Slug | When it fires | Client behaviour |
|---|---|---|
lease-slot-taken | An exclusive booking (or an extension) overlaps an existing pending_payment or active lease. extensions.conflicting_lease_id is included when the overlapping lease could be resolved | Pick a different epoch window or validator. Do not blind-retry, the slot is held |
shared-capacity-full | A shared booking found every seat taken for at least one epoch in the requested range | Pick a different window, or book exclusive when the range frees up |
idempotency-conflict | A request with the same Idempotency-Key is still in flight | Wait per Retry-After (1 second) and retry with the same key |
extension-already-pending | The lease already has an unpaid extension memo | Pay the pending extension, or wait for the rollback sweep to expire it |
extension-race | Two extension attempts raced on the same lease | Retry once |
already-rated | The lease already has a rating | One rating per lease, do not retry |
already-replied | The rating already has a validator reply | One reply per rating, do not retry |
concurrent-rotation | Two API key rotations raced on the same lease | Retry once |
register-nonce-replay | The registration challenge nonce was already used | Generate a fresh nonce and re-sign |
validator-already-registered | The validator pubkey is already registered | Use the existing registration, or contact support if you lost the API key |
lease-already-revoked | Revoke called on a lease that is already revoked | Treat as success, the lease is gone |
payer-is-payout-wallet | POST /v1/solana-pay/{memo} with account equal to the validator's payout wallet | Pay from a different wallet, self-payment would corrupt settlement matching |
422 Unprocessable Entity
| Slug | When it fires | Client behaviour |
|---|---|---|
idempotency-key-reused | Same Idempotency-Key re-sent with a different request body | Use a new key for the new request. Replays must be byte-identical |
shared-not-offered | A mode: "shared" booking against a validator with shared leasing disabled | Book exclusive or pick another validator |
lease-not-active | Booking, credentials, or refund flow hit a lease whose state does not allow the action | Check GET /v1/leases/{id} for the actual state before acting |
lease-expired | The action targets a lease whose paid window has already elapsed | Book a new lease |
extension-exceeds-cap | The extension would push the total span past 30 epochs | Request fewer epochs, the 30-epoch cap is absolute per lease |
rating-not-yet-eligible | Rating attempted before the lease completed or before the minimum usage threshold (10,000 TEE signatures) | Wait until the lease completes with real usage |
invalid-account | POST /v1/solana-pay/{memo} with an account that is not a valid base58 Solana pubkey | Send the payer's pubkey in base58 |
invalid-onboarding-token | The onboard_token passed to registration is invalid | Mint a fresh session from the web funnel |
not-a-solana-validator | The registration pubkey is not a known Solana vote-account identity | Check the pubkey, registration verifies it against the cluster |
register-price-required | Registration without an exclusive price | Set price_sol |
register-invalid-tier | Unknown tier value (allowed: economy, standard, performance) | Fix the tier |
register-invalid-region | Unknown region value | Fix the region |
register-shared-config-incomplete | Shared offering enabled at registration with price or seat cap missing | Provide both shared_price_sol and max_concurrent_shared_leases |
register-shared-price-invalid | Shared price fails validation at registration | Fix the price |
register-shared-seats-invalid | Shared seat cap fails validation at registration (must be at least 1) | Fix the seat cap |
shared-config-incomplete | PATCH /v1/validators/{id} would leave the shared offering half-configured | Send price and seat cap together |
patch-validator-empty | PATCH /v1/validators/{id} with no fields to change | Send at least one field |
429 Too Many Requests
| Slug | When it fires | Client behaviour |
|---|---|---|
rate-limit-exceeded | A token bucket ran dry. Per-route limits: token minting 12/min per identity, lease creation 10/min per trader, validator browse 600/min per IP, everything else 60/min | Honor Retry-After. Pace token refreshes around the roughly 10-minute JWT TTL instead of polling |
5xx Server errors
| Slug | Status | When it fires | Client behaviour |
|---|---|---|---|
database-error | 500 | Unexpected database failure | Retry with backoff |
internal-error | 500 | Unexpected internal failure | Retry with backoff |
jwt-sign-failed | 500 | The KMS signing call failed on the token route | Retry, keep using your current JWT until its exp |
audit-row-encoding-failed | 500 | The token route could not record its audit row | Retry |
token-hash-failure | 500 | API token hashing failed during credentials reveal or rotation | Retry |
admin-user-not-provisioned | 500 | An admin route could not resolve the acting staff user | Staff-only, contact the platform team |
solana-rpc-unavailable | 503 or 502 | Upstream Solana RPC is down. 503 on registration, stake checks, and Solana Pay transaction building, 502 on leader-schedule routes | Retry with backoff, the marketplace itself is healthy |
Booking conflicts in detail
The two booking 409s are deliberately distinct:
lease-slot-takenis the exclusive-overlap conflict. There is one specific lease in your way, and when the API can resolve it you get its ID inextensions.conflicting_lease_id. Apending_paymentlease holds the slot too, so a conflict can clear by itself if that booking goes unpaid past the 15-minute payment window.shared-capacity-fullis the seat-pool conflict formode: "shared". There is no single conflicting lease, the seat count is exhausted for at least one epoch in your range.
A practical booking loop: on lease-slot-taken or shared-capacity-full, re-fetch the validator's availability from GET /v1/validators/{id} and offer the trader the next free window. On shared-not-offered (422), fall back to an exclusive quote.
For extensions, extension-already-pending means your previous extension attempt produced a payment memo that has not settled yet. Pay it or wait for the sweep to roll it back, do not stack new attempts. extension-race is a pure concurrency artifact and is safe to retry once.
Idempotency error semantics
The three guarded POSTs (/v1/leases, /v1/leases/{id}/extend, /v1/validators/{id}/leases/{lease_id}/revoke) interact with errors in a specific way:
- Only
200/201responses are cached and replayed (for 24 hours, byte-identical body required). - An error response releases the key, so retrying with the same key re-runs the handler. This is the correct recovery path after a 5xx.
409 idempotency-conflictmeans a duplicate is in flight right now, retry after 1 second with the same key.422 idempotency-key-reusedmeans you changed the body under a used key, mint a new key.
Full mechanics on the authentication and rate limits page.