Reference
Limits and errors
Rate limits
Limits are per key. Each group is a token bucket: it holds burst tokens and
refills at the per-minute rate; a request takes one token or is answered
429. If the limits do not fit your use, ask us through the portal's contact
address.
| Group | Routes | Per minute | Burst | In flight at once |
|---|---|---|---|---|
| Full | /v1/verify/full… |
6 | 3 | 2 |
| Quick | /v1/verify/quick…, /v1/verify/quick-smell… |
60 | 20 | 20 |
| Status | /v1/verify/status…, /v1/verify/result/…, /v1/verifier/capabilities, /v1/corrections |
300 | 60 | 50 |
Two things are not counted: a submit answered from an existing result
("cached": true, or a 200 straight from a submit) gives its token back,
and status reads always do. Polling is free; poll at the interval the
reply suggests, not faster.
"In flight" counts HTTP requests open at the same time. The asynchronous submit routes return at once, so the Full cap of 2 bounds simultaneous submits, not verifications in progress.
The 429 replies carry Retry-After in seconds:
{"error": "Rate limit exceeded", "retry_after": 7}
{"error": "Too many concurrent requests", "retry_after": 1}
Sizes and timeouts
| What | Limit |
|---|---|
| Submit body | 50 MB (413 above) |
| Batch status body | 1 MB |
| Any other body | 16 KB |
| Time to send a submit body | 120 s |
| Time a synchronous submit may hold the connection | about 5 minutes |
| Time a status call may take | 60 s |
wait_ms on the batch route |
0 to 10000 |
| Connections per client address | 256 |
| Methods | GET, HEAD, POST; 405 otherwise |
Keys
| What | Limit |
|---|---|
| Keys per account | 5 (revoked keys do not count) |
| Scopes on a portal key | verify:invoke, verify:full; verify:share-mode on request |
| Revocation takes effect | within 30 s |
Errors
| HTTP | Body | Cause | What to do |
|---|---|---|---|
400 |
{"detail": "Content-Type must be application/octet-stream"} |
Wrong content type on a submit. | Fix the header. |
400 |
{"detail": "Invalid ValidationRequest: …"} |
The body is not a ValidationRequest. |
Fix the body. |
400 |
{"detail": "Request must be BlockValidation"} |
The request is not a block request. | Send a BlockValidation. |
400 |
{"detail": "ValidationRequest precheck failed"} or … hash mismatch"} |
The proof failed the structural check or its hashes do not match. | Not retryable with the same bytes. |
400 |
{"detail": "target_override must be 32-byte hex …"} |
Malformed X-Target-Override. |
64 hex characters. |
400 |
{"detail": "share-mode nonce is not derived from Proof.Hash: …"} |
The share's header nonce is not the first 4 bytes of the inference digest. | Fix the candidate; not retryable. |
400 |
{"detail": "X-Target-Override is not supported on /verify/quick/request/submit"} |
Share header on the Quick route. | Use the Full or Quick-Smell route for shares. |
400 |
{"status": "refused", "error": "share_mode_not_scoped", …} |
Share header together with X-Verify-Result-Contract. |
Drop the contract header for shares. |
400 |
{"status": "refused", "error": "context_required", …} |
Scoped format without height and ruleset in the request. |
Fill them in. |
400 |
{"detail": "scope_key must be 64 lowercase hex characters"} |
Malformed scope_key. |
Fix it. |
400 |
{"detail": "items must not be empty"} |
Batch status with no items. | |
401 |
{"error": "Unauthorized", "detail": "Missing API key"} or "Invalid API key" |
No key, an unknown key, or a revoked one. | Check the Authorization header; create a new key if revoked. |
403 |
{"detail": "Insufficient permissions. Required scope: verify:full"} |
The key lacks verify:full. |
Portal keys have it. |
403 |
{"detail": "X-Target-Override requires verify:share-mode scope; …"} |
Share header on a key without the scope. | Ask for the scope (Verifying shares). |
404 |
{"detail": "No cached result found"} / … for this hash_id"} |
Nothing known for that hash and type (for shares: the header is missing on the poll). | Submit, or poll with the right type and header. |
404 |
{"status": "unknown", "scope_key": "…"} |
No result and no job for that scope. | Submit again. |
404 |
empty | A path that does not exist here. | See the API reference. |
405 |
empty | Method not allowed. | |
406 |
{"detail": "Only JSON responses are supported"} |
Accept excludes JSON. |
Send Accept: application/json or omit it. |
409 |
{"status": "rejected", "outcome": {"code": "CONTEXT_MISMATCH", …}} |
The request names another network. | Not retryable; mainnet only. |
413 |
{"detail": "Request body too large"} or empty |
Body above the cap. | |
422 |
validation error | A malformed batch item or query parameter. | Fix the request. |
429 |
{"error": "Rate limit exceeded", "retry_after": n} |
Per-key limit. | Wait Retry-After. |
429 |
{"error": "Too many concurrent requests", "retry_after": 1} |
Too many requests open at once. | Wait a second. |
429 |
empty | Connection cap per address. | Reduce parallel connections. |
500 |
{"detail": "Internal server error"} |
Backend fault. | Retry later; tell us if it persists. |
503 |
{"detail": "…"}, Retry-After |
The backend cannot take the request now (no worker, queue full, ownership unavailable). | Retry after the delay. |
503 |
{"status": "pending", "state": "needs_reverify", "outcome": {…}}, Retry-After |
Scoped format: no verdict this time; outcome.code says why and retryable whether to try again. |
Honour retryable and Retry-After. |
504 |
{"detail": "Verification request timed out"} |
Synchronous route; the verification outran the budget. | Use the asynchronous route. |
A 5xx with an HTML body or no body came from the edge, not the backend;
retry with backoff.
Retrying
- Honour
Retry-Afterwhen present. Otherwise back off exponentially from a few seconds, capped at a minute. - A
202is not an error: poll again afterretry_after_seconds(scoped format) or 5–10 seconds (plain format). - A
4xxother than429will not succeed by retrying the same bytes. - Submitting the same request while it is pending is harmless: the service
answers
202and does not start a second run.
Updated 2026-10-07