Skip to main content

API Conventions

Every operation in API v2 follows the same transport contract. Learn it once and it applies everywhere.

Base URL and versioning

The version is in the path. Every response carries X-Blank-Api-Version: 2. Paths outside /api/v2 are internal Blank planes with no stability contract — do not call them.

Request headers

JSON request bodies are capped at 1,048,576 bytes. A larger body returns 413 request_body_too_large; an unparseable body returns 400 malformed_json. Version-guarded mutations return 428 precondition_required when If-Match is absent, 400 precondition_invalid when it is not one strong positive-integer ETag, and 412 precondition_failed when a well-formed ETag is stale.

Response headers

The SDK surfaces all of this on result.metadata:
Log X-Request-Id with every failure. It is the identifier Blank needs to trace what happened.

Pagination and cursors

List operations return a cursor page:
  • cursor — opaque, signed, and bound to the query that produced it. Never construct, decode, or mutate one. Changing filters mid-traversal invalidates the cursor.
  • limit — integer from 1 to 100, default 50.
  • Stop when hasMore is false or nextCursor is null.
  • A malformed, tampered, or mismatched cursor returns 400 cursor_invalid.
There are no offsets and no total counts. Ordering is stable and documented per operation.
The SDK ships bounded async iterators for high-volume traversal:
maxPages defaults to 100 and is capped at 10,000. The iterator throws rather than looping forever once the bound is reached, so pick a value that matches the dataset you expect. Two list responses are not cursor paginated because they are hard-capped: the staking leaderboard and prediction standings each return at most 100 entries as { data: [...] }.

Rate limits

Every operation is assigned a named rate policy, reported in X-RateLimit-Policy: public-read-cheap, public-read-expensive, api-key-read, operation-read, prediction-submit, transaction-prepare, transaction-submit, webhook-admin, export Each policy is enforced across several dimensions at once. Anonymous requests are limited globally and per client IP. Authenticated requests are additionally limited per API key, per account, and per wallet. The response headers always describe the tightest dimension currently applying to you.
Limits are environment configuration and change without a contract change. Do not hardcode numbers — read X-RateLimit-Remaining and X-RateLimit-Reset, and honour Retry-After.
Exceeding a limit returns 429 rate_limit_exceeded with Retry-After and the rate-limit headers. If the limiter itself cannot be reached, requests fail closed with 503 rate_limit_unavailable, which is retryable.

Caching and conditional requests

Anonymous reads are cacheable and carry an ETag. Send it back on the next request:
A match returns 304 Not Modified with no body and does not consume bandwidth. Cache lifetimes vary by operation, from max-age=2 for market snapshots and trades up to max-age=300 for the Solana manifest. Authenticated reads are always private, no-store, and so is every error response. Never put them in a shared cache. Some authenticated resources still return an ETag — but it is the resource’s integer version, used with If-Match for optimistic concurrency, not a caching hint.

Idempotency

Every mutation requires an Idempotency-Key header. There are no exceptions.
  • Format: 8 to 128 printable ASCII characters.
  • Retention: 24 hours per key, scoped to the API key identity, HTTP method, and route.
  • The claim, the state transition, the stored response, and the outbox record commit atomically in Postgres, so a replay returns exactly what the first attempt returned.
The SDK generates a key when you do not supply one and always exposes it:
For anything you must be able to reconcile, generate the key yourself and persist it before the request. A generated key is lost if your process dies mid-call, and you cannot safely replay what you cannot name.
BlankApiError and BlankNetworkError both carry idempotencyKey, so an uncertain mutation can always be retried safely with the same key.

Optimistic concurrency

Resources that can be updated concurrently carry an integer version. Guarded mutations require a strong If-Match header quoting that version:
The SDK takes the number and formats the header for you:
A stale version returns 412 precondition_failed for webhook endpoints and 412 transaction_intent_version_conflict for transaction intents. Re-read the resource, re-apply your change, and retry.

Retries and deadlines

The SDK retries at most twice, with full-jitter backoff, for:
  • network failures and timeouts
  • 429, 502, 503, and 504
A mutation is retried only when it carries an idempotency key. Automatic waits are capped at 30 seconds; a longer Retry-After is surfaced to you instead of being slept through. Deadlines are per attempt, not per call:
  • timeoutMs — per-attempt deadline. Default 30,000; valid range 100 to 120,000.
  • retries0, 1, or 2. Defaults to 2, and can be set per client or per call.
  • signal — an AbortSignal that cancels the call and any pending backoff.
Both can be set once on the client:
If you implement retries yourself, use exponential backoff with jitter, cap total attempts, and always reuse the same idempotency key on a mutation. Do not retry a 4xx except 429 or 409 idempotency_request_in_progress; both carry Retry-After.

Numeric precision

Prices, SOL amounts, and other decimal quantities are canonical decimal strings such as "0.00001234". Raw on-chain amounts and lamport values are unsigned integer strings such as "1000000000". They are strings on purpose: IEEE 754 doubles cannot represent them exactly. Parse them with BigInt or a decimal library, never with Number.

Response validation

The SDK validates every success response against the generated operation schema before returning it. It also validates every error’s HTTP status, code, canonical title, and type URI against that operation. An invalid success raises BlankNetworkError; an invalid error becomes BlankApiError with invalid_error_response. This is why the SDK version should track the API contract — see the OpenAPI reference.

Errors

Every error is RFC 9457 application/problem+json. See Errors for the envelope and the full code catalogue.