Errors and retries

What the SDK raises, what each status code means, and what it retries for you.

HTTP endpoints on this page

  • GET/health

Parameters and response shapes for each are in the API reference.

Every non-2xx response becomes a PulseAPIError. It carries the HTTP status and the server's own detail string, which is usually specific enough to act on without looking anything up.

PulseAPIError

python
from simudyne import PulseAPIError

try:
    client.simulation.get_sim_params("not-a-real-sim-id")
except PulseAPIError as exc:
    print(f"status: {exc.status_code}")
    print(f"detail: {exc.detail}")
Output
status: 404
detail: Simulation 'not-a-real-sim-id' not found
AttributeTypeDescription
status_codeintThe HTTP status
detailstrThe server's detail field, or the raw body if it was not JSON
errorslist or NoneThe API's top-level errors list, when the response has one. The 422 for a bad provider or exchange puts its field-level errors here. None otherwise

For a 422, the message is written to be read: it names each field that failed and why, rather than printing the raw list of validation errors.

It is importable from the package root, so from simudyne import PulseAPIError is all you need. The string form reads API Error (404): Simulation '…' not found, which is what an uncaught traceback shows.

Two SDK methods raise plain Python exceptions rather than PulseAPIError, because they fail before any request is made: validation.run() raises ValueError when it is given neither sim_ids nor sim_files, an empty list, or more than 25 simulations in total, and PulseABM() raises ValueError when no API key is set.

Status codes

StatusMeaningWhat to do
401The key is missing, unknown, inactive or expired — or the account is inactiveCheck the key. client.api_keys.list() shows which of yours are active
403Your tier does not include this endpoint. The detail names the tier and links hereNot retryable. See Plans for what each tier includes
404Not found, or not yours — a job or simulation belonging to another account reads as absent rather than forbidden. Also an unknown foundation model name passed as model_idCheck the id. For a missing output file the detail says whether the run failed, succeeded without producing it, or is still queued. fm.models() lists the model names
400A malformed request the schema cannot express — an unknown filename, an empty sim_ids list, an unknown model_args key, or a model_id that matches more than one foundation modelThe detail names what was rejected and often lists the valid options. For an ambiguous model, pass the exact model_id
422The request was understood but cannot run. This covers a missing required field, a bad provider or exchange, an unknown or out-of-range scenario_params key, a field sent to the wrong engine (scenario on an FM run, model_id on an ABM run), an FM n_runs above 8, an unknown validation config field, an ABM run on a day that is not calibrated, or an FM run on data that does not exist or that the model does not acceptNot retryable. Fix the field the detail names. For an uncalibrated day or missing data, the detail lists the dates that are available
429Free-tier download quota exhausted for the rolling window. Nothing was chargedRetried automatically (see below). Check profile.downloads() for when it rolls over
502 / 503An upstream service — the simulation engine, the FM orchestrator — is unreachableRetried automatically. Transient
504The upstream took too longRetried automatically

A 422 on provider or exchange

These two fields became required, and the HKEX provider was renamed from hkex to omd. Rather than a bare validation error, the API answers with the valid options:

text
A recent change made 'provider' and 'exchange' required, and renamed the
HKEX provider to 'omd' (use 'omd', not 'hkex'). Valid providers: omd, bmll.
Valid exchanges: hkex_securities, hkex_derivatives, hkex_derivatives_mca, lse.

Automatic retries

The client retries transient failures itself, with exponential backoff, before raising. You do not need to wrap calls in a retry loop for these.

SettingDefaultDescription
max_retries3Retry attempts after the first try, so at most 4 requests
timeout30Per-request timeout in seconds
Retried statuses429, 502, 503, 504Everything else raises immediately
Backoff1s, 2s, 4s2 ** attempt seconds between tries

Connection errors and timeouts are retried on the same schedule and re-raised as the underlying requests exception if every attempt fails.

python
from simudyne import PulseABM

# More patience for a slow link
patient = PulseABM(timeout=120, max_retries=5)

# No retries at all, for a script that should fail fast
strict = PulseABM(max_retries=0)

Whatever a client was built with, it will tell you:

python
print(f"timeout={client.timeout}s max_retries={client.max_retries}")
print(f"retries on: {sorted(type(client).RETRYABLE_STATUS_CODES)}")
Output
timeout=30s max_retries=3
retries on: [429, 502, 503, 504]

Retries apply to the HTTP request, not to a simulation. A job that fails in the engine returns a successful HTTP response with has_errors: true — read the job status and fetch get_job_logs(job_id) for the reason.

Client configuration

ArgumentEnvironment variableDefault
api_keySIMUDYNE_API_KEYrequired — ValueError if neither is set
base_urlSIMUDYNE_BASE_URLhttps://pulse-api.simudyne.com
timeout—30 seconds
max_retries—3
python
from simudyne import PulseABM

# Both arguments fall back to the environment when omitted, so this is
# equivalent to PulseABM() with SIMUDYNE_API_KEY and SIMUDYNE_BASE_URL set.
client = PulseABM(
    api_key="pk_live_...",
    base_url="https://pulse-api.simudyne.com",
)

The underlying requests.Session is exposed as client.session with the auth header already applied, which is how you reach anything the SDK does not wrap — see the API reference.

Handling errors by kind

Most code only needs to separate “my fault” from “their fault”:

python
from simudyne import PulseAPIError

def fetch(sim_id):
    try:
        return client.simulation.get_sim_data(sim_id)
    except PulseAPIError as exc:
        if exc.status_code == 404:
            return None                      # not there; carry on
        if exc.status_code == 403:
            raise SystemExit(f"tier: {exc.detail}")   # never going to work
        if exc.status_code == 429:
            quota = client.profile.downloads()
            raise SystemExit(f"quota exhausted, {quota['window_hours']}h window")
        raise                                # anything else is a real bug

sim_id = client.simulation.list_cached()["simulations"][0]["example_sim_id"]
print(fetch("nope") is None, fetch(sim_id) is not None)