Simulix

Errors

Every Simulix response uses a consistent envelope. Every 4xx and 5xx carries a machine-parseable error code (your SDK dispatches on this, not on the HTTP status) plus a correlation_id you can hand to support.

Key takeaway
Every Simulix response uses a consistent envelope. Every 4xx and 5xx carries a machine-parseable error code (your SDK dispatches on this, not on the HTTP status) plus a correlation_id you can hand to support.

The response envelope

Every endpoint — success or failure — returns the same shape: a top-level data field with the resource (or null on error), an error field with the machine-parseable error code (or null on success), and a meta object carrying request_id, pagination cursors, rate-limit headers mirrored into the body, and correlation_id on 5xx responses. Your SDK dispatches on the error string, never on the HTTP status code alone; status codes are correct but coarse-grained, error codes are stable across versions.

400 — validation_error

Request body, query parameters, or path parameters failed Pydantic validation. The error message in meta describes which field and what the violation was (e.g. agent_count must be >= 1 and <= 100000). Fix on the client side before retrying; the same request body will always fail.

401 — authentication errors

Three distinct codes. missing_authorization fires when no Authorization header was sent. invalid_credentials fires when the bearer token doesn't match any active key (revoked, expired, malformed). not_authenticated fires from the session-cookie path when the dashboard is reached without a logged-in user. All three return 401; differentiate via the error code to surface the right UI affordance (sign in vs rotate key vs re-login).

403 — authorization errors

Authenticated but not allowed. forbidden is the generic deny. forbidden_missing_scope means the API key was minted without the scope the endpoint requires (most often training:write on /v1/training routes; mint a new key with the right scope or contact your org admin). forbidden_operator_only means the endpoint is in the /v1/admin/* family and requires a key on the operator allowlist — customer keys do not access these. The meta.required_scope field on missing-scope responses tells you which scope to add.

404 — resource not found

Three codes, all 404. not_found is the catch-all (route not registered, path typo). simulation_not_found and workflow_not_found are more specific — they fire when the resource genuinely does not exist in your org's data plane OR exists in a different org (Simulix never leaks the difference; cross-org lookups always return 404, not 403, so attackers cannot probe for resource existence).

409 — idempotency conflicts

Three codes in the idempotency family. idempotency_key_required means a POST create endpoint was hit without an Idempotency-Key header (always required on POST; safe to retry the request after adding the header). idempotency_key_invalid means the key format was rejected (must be a ULID, UUID, or 32+ char random string). idempotency_conflict means the same key was reused within the 24-hour window with a different request body — Simulix preserves the original response shape for safe retries but blocks divergent reuse to prevent silent double-creates.

422 — business rule violations

Authenticated, authorized, well-formed — but the request violates a plan-tier or workflow business rule. tier_cap_exceeded means the org is over its monthly run budget (upgrade the plan or wait until the next billing cycle). agent_count_exceeds_plan means a single simulation requested more agents than the tier allows (Sandbox 500, Studio 10,000, Enterprise contracted; reduce agent_count or upgrade). simulation_not_complete fires on GET /v1/simulations/:id/results when the run is still in progress — poll the status endpoint until completed before pulling results.

429 — rate limit

rate_limit_exceeded means the token bucket for this API key is empty. The response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and a Retry-After header — honor Retry-After before retrying (the SDK does this for you). rate_limiter_unavailable is a 503-class fallback returned when the rate-limiter backend itself is degraded; treat it as transient + retry with exponential backoff.

5xx — server errors

Every 5xx response includes a correlation_id in meta. Hand that ID to founders@simulix.com or via the operator console and we can trace the request through the full stack (load balancer, API worker, simulation worker, downstream LLM call, database transaction). Do not assume a 5xx means your request was malformed — they almost always indicate a Simulix-side issue, and the correlation_id is what we need to fix it.

503 — maintenance + scheduled outage

Returned during planned maintenance windows announced via /changelog and the status page. The Retry-After header indicates when traffic resumes. The synthetic-down state on the customer status page renders fast (per F-F8 launch protections) so you can confirm an outage is real without waiting on a hung probe.

Billing errors

billing_disabled fires on endpoints that require an active Stripe customer (most simulation endpoints) when the org has no card on file — direct the user to /dashboard/billing to add one. billing_misconfigured is a 5xx-class internal error when our Stripe webhook integration is out of sync with the database — treat as transient + alert via support if persistent.

Other domain errors

email_delivery_failed fires on the magic-link signup path when Resend rejects the destination (typo, hard-bounce). Surface the literal Resend error message to the user; do not silently retry — the address is most likely invalid.

Reading errors safely

Three rules. (1) Always dispatch on error code, not HTTP status — the status is right but loses precision when multiple codes share a status. (2) Always log meta.correlation_id on 5xx — without it we cannot help you. (3) Always check meta.request_id on every response — it lets you correlate your client logs with our server logs even when the request succeeded but the downstream behavior looked wrong.