Skip to main content

Troubleshooting

Every failure carries a stable code and a requestId. Start there: read code from the Problem Details body and log X-Request-Id.

Authentication and scopes

The credential is missing, malformed, unknown, expired, revoked, issued for a different environment, or its wallet is not verified. The API deliberately does not tell you which.Check in this order: the header is Authorization: Bearer <key>; the key matches blank_live_... for production or blank_test_... for staging and development; expiresAt is in the future. Then rotate the key in Settings and redeploy. See Authentication.
The key is valid but does not carry every scope the operation declares. Scopes cannot be widened after creation — create a new key with the right scope set and revoke the old one. Check what a key actually grants with blank.identity.me(). See Scopes.
BlankClient refuses an apiKey in a browser runtime, and rejects a baseUrl that carries credentials, a query, or a fragment, or that is not HTTPS outside localhost. Move the client to server code. If a browser needs private data, proxy it through your own server.
Only anonymous read operations are browser-reachable. Authenticated operations receive no Access-Control-Allow-Origin, by design — an API key must never reach a browser. Call them from your server.
Key creation requires a username on the Blank account, and each account can hold at most 5 active keys per environment. Set a username, or revoke an unused key first.

Requests and responses

No route matched the method and path. Confirm both against the HTTP reference, and confirm the origin is https://api.blank.build/api/v2. There is no v1 contract and no unversioned alias — a path outside /api/v2 will never serve the public contract.
The request failed schema validation. Read errors[]: each entry has a path such as body.predictedPriceInSol, a code, and a message. Request schemas are strict, so an unexpected extra property is a validation failure, not a silently ignored field.
Requests with a body must send Content-Type: application/json and well-formed JSON. 413 request_body_too_large means the body exceeded 1,048,576 bytes.
Prices and SOL amounts are canonical decimal strings; raw on-chain amounts and lamports are unsigned integer strings. They are strings because doubles cannot represent them exactly. Parse with BigInt or a decimal library — never Number.
The SDK validates every response against the contract schema and refuses to hand back data that violates it. It also raises BlankNetworkError for aborts (request_aborted), per-attempt timeouts (request_timeout), and transport failures (network_error). A BlankNetworkError means the outcome is unknown — for a mutation, retry with the same idempotencyKey exposed on the error.

Pagination

Cursors are opaque, signed, and bound to the query that produced them. This error means the cursor was hand-built, decoded and re-encoded, truncated, or reused after you changed a filter. Restart the traversal without a cursor and keep the query identical across pages.
Stop when page.hasMore is false or page.nextCursor is null. If you use iterate(), it throws once maxPages (default 100, maximum 10,000) is exceeded rather than looping forever — raise the bound deliberately or narrow the query.
The staking leaderboard and prediction standings are hard-capped at 100 entries and are not paginated. That is the full response.

Rate limits

Read Retry-After and wait. X-RateLimit-Policy names the policy that tripped, and X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset describe the tightest dimension applying to you.Anonymous traffic is limited globally and per IP; authenticated traffic is additionally limited per API key, per account, and per wallet — so more keys on the same account will not raise an account-level ceiling. Limits are environment configuration: read the headers, do not hardcode numbers.
The limiter itself could not be reached, and requests fail closed rather than passing unmetered. It is transient — retry with backoff.

Idempotency and concurrency

Every mutation requires an Idempotency-Key of 8 to 128 printable ASCII characters. The SDK generates one when you omit idempotencyKey, so this usually means a hand-built HTTP request omitted the header.
The same key was replayed with a different request body. Keys are scoped to the API key identity, method, and route, and retained for 24 hours. Either resend the byte-identical request, or use a new key.
Your first attempt is still running. Wait for the Retry-After: 1 window and retry with the same key. Do not switch keys — that would create a second mutation.
That is exactly what idempotency keys are for. Retry the same request with the same key: a completed attempt replays its original response with Idempotent-Replayed: true. Generate and persist the key before the request, so a crash mid-call still leaves you able to reconcile.
If-Match did not match the resource’s current version — someone else changed it first. Re-read the resource, re-apply your change on top of the new state, and retry with the new version.

Price predictions

The end user must sign the exact UTF-8 message returned by createDelegatedPredictionIntent with the wallet named in that intent. Do not rebuild, trim, prefix, or otherwise rewrite the message before calling the wallet.
Submit the intent with the same API key that created it. Blank deliberately returns the same result for an unknown ID and an intent owned by another key.
Intents expire after five minutes or at round lock and can be used once. Retry an uncertain submission with the original idempotency key; otherwise prepare a fresh intent.
walletAddress in the body must equal the wallet pinned to the API key. Read it from blank.identity.me() rather than configuring it separately.
Predictions are only accepted while the round is open, before locksAt. Re-read the round; if it has moved to locked, wait for the next one.
One prediction per wallet per round, and predictions are immutable — they cannot be edited or withdrawn. Read yours back with blank.predictions.get(predictionId).
The Blank account behind the key needs a username, and the pinned wallet must be verified. Fix both in the Blank app, then re-check with blank.predictions.eligibility(mint, roundId).
Eligibility is evaluated against finalized chain state. A transfer that has not finalized yet will not count. Check balanceRaw and evaluationSlot on the eligibility response.
The token has no Price Prediction allocation. That allocation is fixed at launch and cannot be added later. See Price Prediction.
predictedPriceInSol must be a positive canonical decimal string — not "0" — with at most 12 integer digits and 18 fractional digits. Send a string, not a number.
Exact predictions are withheld until the round durably locks, so nobody can copy live entries. Before lock, listRoundPredictions returns { visibility: "aggregate", participantCount } and standings return this error. After lock, entries return visibility: "exact". Your own prediction is always readable via getOwnedPrediction.
Finalized balance data is temporarily unreadable. Retry with backoff; it is not a rejection.

Transaction intents

The signed message differs from the message Blank prepared. This is almost always a wallet rewriting the fee payer, blockhash, compute-budget instructions, or lookup tables before signing. Disable transaction “enhancement” features, or sign the exact bytes from serializedTransaction.
The transaction was signed by a wallet other than expectedSigner. That field is the wallet pinned to the API key.
The blockhash window closed before a valid submission arrived. Blockhashes are short-lived: fetch, inspect, sign, and submit in one pass. Request a fresh intent — a signature cannot be moved to a new intent.
The If-Match version was stale. Re-read the intent and submit with its current version.
202 means durably queued, not confirmed. Poll blank.transactionIntents.get(intentId) honouring Retry-After until status is confirmed, failed, or expired. See Operations and transaction intents.

Webhooks

Endpoint URLs must be public HTTPS on the default port, with no userinfo and no fragment, and must not resolve to a private, loopback, link-local, or reserved address. Hostnames ending in .localhost, .local, .internal, .home.arpa, or .onion are rejected. The policy is re-applied to every redirect hop.
Verify the raw request body before parsing JSON. Re-serializing changes the bytes and therefore the HMAC. Also check that you are within the 300-second timestamp tolerance and that your framework has not consumed or rewritten the body.
During the overlap window Blank signs with both secrets: v1 with the new secret and v0 with the previous one. Pass both to verifyWebhookSignature via secret and previousSecret until the window closes.
All 7 attempts failed. Inspect responseStatus and lastErrorCode on the delivery: http_<status> is your endpoint rejecting the request, request_timeout means you exceeded the 10-second delivery timeout, and url_disallowed or redirect_limit_exceeded are URL policy failures. Fix the endpoint, then drain with blank.webhooks.replay(deliveryId).
Delivery is at-least-once. Deduplicate on Blank-Event-Id, which is stable across retries and replays; Blank-Delivery-Id changes each attempt.
The delivery timeout is 10 seconds. Return 2xx as soon as the event is durably written to a queue or table, and do the real work asynchronously.

Still stuck

Collect the requestId, the code, and the operation ID, and report them together. Never share an API key, a webhook signing secret, or a signed transaction.