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 | Python exception | When |
|---|---|---|
| 200 | none | Success: the result is in the body. |
| 202 | none | Accepted: a simulation job was queued; poll it for the result. |
| 400 Bad Request | ValidationError | The 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. |
| 401 Unauthorized | AuthError | No key was sent (the body carries keyUrl), or the key is not one we issued, or was revoked. Never about the allowance. |
| 402 Payment Required | QuotaError | The account's monthly allowance is spent, on every metered route. Nothing was counted. Retry-After gives the seconds until resetAt. |
| 403 Forbidden | AuthError | The job belongs to someone else, or the account's plan does not carry the batch endpoint or the solve mode asked for. |
| 404 Not Found | NotFound | No such calculator slug, job type or job. Nothing is counted. |
| 409 Conflict | APIError | A device approval at the key limit, or for a code already decided (a signed-in browser call). |
| 422 Unprocessable Content | ValidationError | The 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. |
| 429 Too Many Requests | RateLimitError | Too many requests in a minute for the credential, or too many jobs queued or running at once. Retry-After says when to try again. |
| 500 Internal Server Error | APIError | The result was computed but could not be returned, or a solve failed. Nothing was charged. |
| 503 Service Unavailable | APIError | The key or the allowance counter could not be checked, or a job could not be queued. Nothing was queued or charged; try again. |
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.
{
"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"
}{
"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.
{
"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.
{
"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.
{
"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.
| errorKind | Meaning |
|---|---|
| invalid_request | A 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_large | The request is within the contract but beyond what this tier's lane can run. Reduce the size of the problem. |
| not_available | The request asks for a mode or a feature this tier does not carry. The message names what the tier does include. |
| rate_limited | A concurrent-job limit or a monthly allowance is spent. Carries Retry-After, and resetAt when the allowance is monthly. |
| timeout | The job exceeded its type's time ceiling. Nothing is charged for it. |
| interrupted | The run stopped before it finished — a worker was replaced or a task was stopped. Nothing is charged for it. |
| result_expired | The job completed, but its result is no longer stored. The status still reads completed and carries no resultUrl. |
| transient | The service could not accept or check the request just now. Nothing was queued or charged; try again. |
| fault | The service failed while computing. The message says which step; nothing is charged for it. |