Skip to content
Documentation sections

Errors

Every refusal carries errorKind beside detail, and the fields to act on. Branch on the status and errorKind; the wording of detail is written for people and may change.

The Python library raises a typed exception for each status, chosen by the status alone, never by the message. A 5xx is retried with backoff before it is raised.

Status codes

Status codes and the Python exception for each
StatusPython exceptionWhen
200noneSuccess: the result is in the body.
202noneAccepted: a simulation job was queued; poll it for the result.
400 Bad RequestValidationErrorThe request cannot be answered as sent: an input that is not a finite number, a solve that cannot be set up, a job body outside its contract, or a batch that is empty or over its limit. Nothing is counted.Fields: errorKind, detail, inputs, param, allowed
401 UnauthorizedAuthErrorNo key was sent (the body carries keyUrl), or the key is not one we issued, or was revoked. Never about the allowance.Fields: errorKind, detail, keyUrl
402 Payment RequiredQuotaErrorThe account's monthly allowance is spent, on every metered route. Nothing was counted. Retry-After gives the seconds until resetAt.Fields: errorKind, reason, limit, used, resetAt, upgradeUrl, overageUrl, Retry-After
403 ForbiddenAuthErrorThe job belongs to someone else, or the account's plan does not carry the batch endpoint or the solve mode asked for.Fields: errorKind, detail, requiredTier, upgradeUrl
404 Not FoundNotFoundNo such calculator slug, job type or job. Nothing is counted.Fields: errorKind, detail
409 ConflictAPIErrorA device approval at the key limit, or for a code already decided (a signed-in browser call).Fields: errorKind, detail, reason
422 Unprocessable ContentValidationErrorThe body does not parse: a required field is missing, or a value is of the wrong type (an input sent as text). Answered before the route runs.Fields: detail[].loc, detail[].msg, detail[].type
429 Too Many RequestsRateLimitErrorToo many requests in a minute for the credential, or too many jobs queued or running at once. Retry-After says when to try again.Fields: errorKind, limit, windowSeconds, pending, Retry-After
500 Internal Server ErrorAPIErrorThe result was computed but could not be returned, or a solve failed. Nothing was charged.Fields: errorKind, detail
503 Service UnavailableAPIErrorThe key or the allowance counter could not be checked, or a job could not be queued. Nothing was queued or charged; try again.Fields: errorKind, detail

Two more exceptions are raised without a status: JobFailed, when a job ends in failed (it carries the job's errorKind as .kind), and LoginError, when rftools login delivers no key.

Refusal bodies

These are the bodies the API sends, with this month's figures read from the tier table.

401 · No key sent
{
  "detail": "An API key is required. Get a free key at https://rftools.io/dashboard/?newKey=1.",
  "errorKind": "invalid_request",
  "keyUrl": "https://rftools.io/dashboard/?newKey=1"
}
401 · A key that is not valid
{
  "detail": "This API key is not valid. It may have been revoked.",
  "errorKind": "invalid_request"
}

Malformed, unknown or revoked. A 401 never means the allowance is spent.

402 · The allowance is spent
{
  "detail": "This account's 100 API calls for September are used. They reset on 2026-10-01. The API plan includes 10,000 a month.",
  "errorKind": "rate_limited",
  "reason": "allowance",
  "limit": 100,
  "used": 100,
  "upgradeUrl": "https://rftools.io/pricing/",
  "overageUrl": null,
  "resetAt": "2026-10-01T00:00:00+00:00"
}

With Retry-After in seconds until resetAt. The same body on every metered route, calculators included. overageUrl is set where the plan can turn pay-as-you-go on; reason is overage_cap once that cap is spent too. Nothing was counted.

429 · Too many requests in a minute
{
  "detail": "More than 30 requests in 60 seconds from this address. An API key allows 120.",
  "errorKind": "rate_limited",
  "limit": 30,
  "windowSeconds": 60,
  "upgradeUrl": "https://rftools.io/pricing/"
}

With Retry-After. The limit follows the credential the request carries.

400 / 409 · No room for another key
{
  "detail": "This account already holds 1 API key, the most the Free plan allows. Revoke a key to free its place, or upgrade to Pro for 3 API keys.",
  "errorKind": "not_available",
  "keyLimit": 1,
  "manageUrl": "https://rftools.io/dashboard/#api-usage",
  "upgradeUrl": "https://rftools.io/pricing/"
}

POST /apikeys answers 400; the device approval answers 409 with reason: key_limit.

Error kinds

errorKind is a closed set, on a refused request and on a simulation job that failed after it was accepted. Branch on it rather than on the wording of the message.

Error kinds
errorKindMeaning
invalid_requestA parameter is missing, of the wrong type, out of range, not one of the declared options, or not a parameter of this job type. The 400 body lists every offending parameter.
too_largeThe request is within the contract but beyond what this tier's lane can run. Reduce the size of the problem.
not_availableThe request asks for a mode or a feature this tier does not carry. The message names what the tier does include.
rate_limitedA concurrent-job limit or a monthly allowance is spent. Carries Retry-After, and resetAt when the allowance is monthly.
timeoutThe job exceeded its type's time ceiling. Nothing is charged for it.
interruptedThe run stopped before it finished — a worker was replaced or a task was stopped. Nothing is charged for it.
result_expiredThe job completed, but its result is no longer stored. The status still reads completed and carries no resultUrl.
transientThe service could not accept or check the request just now. Nothing was queued or charged; try again.
faultThe service failed while computing. The message says which step; nothing is charged for it.