Skip to content
Go to Boltz API

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.

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):

StatusSDK exceptionMeaning
400BadRequestErrorMalformed request.
401AuthenticationErrorMissing or invalid API key.
403PermissionDeniedErrorKey lacks permission for this action/workspace.
404NotFoundErrorResource does not exist.
409ConflictErrorConflicting concurrent request.
422UnprocessableEntityErrorValidation failed (see details).
429RateLimitErrorToo many requests — see Rate limits.
≥500InternalServerErrorServer-side error; safe to retry.
APIConnectionErrorNetwork failure or timeout (no HTTP response).
import boltz_api
from 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.body
except boltz_api.APIConnectionError:
print("Could not reach the API")

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:

StatusApplies toMeaning
pendingallQueued, not started.
runningallIn progress; partial results may already be available.
succeededallCompleted.
failedallThe run errored — see the error field.
stoppeddesign & screen onlyYou 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)

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:

ScopePredictionsPipeline runs
Per organization5,000500
Per workspace1,000100

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.

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=...).

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.