Errors & retries
Read API error responses, tell a failed run from a stopped one, handle rate limits, rely on automatic retries, and submit jobs idempotently.
There are two places an error can show up: the HTTP response to a request you make, and the error field on a run that didn't succeed. They use the same { code, message } shape.
HTTP errors
Section titled “HTTP errors”Failed requests return a non-2xx status with a JSON body:
{ "code": "unauthorized", "message": "Invalid or missing API key"}code is a stable, machine-readable string (e.g. unauthorized, forbidden, not_found, bad_request, conflict, internal_error); message is human-readable; some errors also include a details object with field-level information. The Python SDK raises a typed exception per status code, exposing status_code, message, and the parsed body (e.body):
| Status | SDK exception | Meaning |
|---|---|---|
| 400 | BadRequestError | Malformed request. |
| 401 | AuthenticationError | Missing or invalid API key. |
| 403 | PermissionDeniedError | Key lacks permission for this action/workspace. |
| 404 | NotFoundError | Resource does not exist. |
| 409 | ConflictError | Conflicting concurrent request. |
| 422 | UnprocessableEntityError | Validation failed (see details). |
| 429 | RateLimitError | Too many requests — see Rate limits. |
| ≥500 | InternalServerError | Server-side error; safe to retry. |
| — | APIConnectionError | Network failure or timeout (no HTTP response). |
import boltz_apifrom boltz_api import Boltz
client = Boltz()
try: client.predictions.structure_and_binding.start(model="boltz-2.1", input={...})except boltz_api.UnprocessableEntityError as e: print("Invalid input:", e.message)except boltz_api.APIStatusError as e: print(e.status_code, e.message) # the machine-readable `code` is in e.bodyexcept boltz_api.APIConnectionError: print("Could not reach the API")Run failures
Section titled “Run failures”Prediction and pipeline runs are asynchronous, so a request that returns 200 only means the run was accepted. Poll the run and check its status:
| Status | Applies to | Meaning |
|---|---|---|
pending | all | Queued, not started. |
running | all | In progress; partial results may already be available. |
succeeded | all | Completed. |
failed | all | The run errored — see the error field. |
stopped | design & screen only | You stopped it; partial results are kept and it can be resumed. |
stopped only applies to pipeline runs (protein/small-molecule design and library screens). Structure-and-binding and ADME predictions never return stopped.
When a run failed, its error field is populated (it is null otherwise), using the same shape as an HTTP error:
run = client.protein.design.retrieve(run_id)if run.status == "failed": print(run.error.code, run.error.message)Rate limits
Section titled “Rate limits”The API enforces two kinds of limits. Both reject excess submissions with 429 (RateLimitError).
Request rate — 100 requests per 10-second sliding window, scoped per organization (per user for not-yet-org-bound JWTs). Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers; 429 responses also include Retry-After (seconds). Health and docs endpoints are exempt.
Queue caps — caps on concurrent pending (queued, not yet started) submissions per scope. New submissions are rejected once any cap is reached; queued work continues to run, and test-mode submissions are exempt. Defaults:
| Scope | Predictions | Pipeline runs |
|---|---|---|
| Per organization | 5,000 | 500 |
| Per workspace | 1,000 | 100 |
These are the platform defaults. To request higher limits for your organization, contact support@boltz.bio.
The SDK retries 429s automatically (see Automatic retries) and honors Retry-After, so you usually only surface a RateLimitError after retries are exhausted. If you drive the API yourself, back off when you see a 429 rather than retrying immediately.
Automatic retries
Section titled “Automatic retries”The SDK retries transient failures 2 times by default with short exponential backoff (≈0.5s, doubling, capped at 8s, with jitter). It retries connection errors, request timeouts (408), 409, 429, and ≥500 — and honors the server's Retry-After and x-should-retry hints. The other 4xx errors (400/401/403/404/422) are not retried, since retrying won't change the outcome.
# Disable, or raise, the retry count — globally or per request.client = Boltz(max_retries=0)client.with_options(max_retries=5).protein.design.start(...)Requests time out after 60 seconds by default (raising APITimeoutError, which is itself retried). Configure it with Boltz(timeout=...) or with_options(timeout=...).
Idempotency
Section titled “Idempotency”Retrying a job submission shouldn't create a duplicate run. Pass an idempotency_key (a string you choose) when you start a job: if a run with that key already exists in your workspace, the API returns the existing run instead of creating a new one — and doesn't bill you twice.
client.protein.design.start( target={...}, binder_specification={...}, num_proteins=100, idempotency_key="my-run-2026-05-31",)With the CLI, use the top-level --idempotency-key flag. The key is scoped to your organization, workspace, job type, and mode (test vs live), so the same key can be reused across test and live or across different job types. Reusing an idempotency key with a differing request body returns a ConflictError.