| Status | Meaning | Retry? | What to do |
|---|---|---|---|
400 Bad request | Valid JSON, but outside a limit (too many options or levels, over 64k tokens), an unknown model or a bad image | No | Fix the request; the message names the limit |
401 Unauthorized | Missing or invalid API key | No | Check the Authorization header and that the key is not revoked |
402 Out of credit | The workspace's prepaid balance is zero. Not billed | After top-up | Add credit in the console, or turn on auto reload, then retry |
403 Safety refusal | Refused by the safety check: the request asks for a use the Acceptable Use Policy forbids. Rolling out in shadow mode | No | Change what the request asks; see Responsible use |
422 Unprocessable | The body failed validation | No | Fix the fields listed in detail |
429 Too many requests | Over your rate limit | Yes | Wait Retry-After seconds, then retry |
529 Overloaded | No capacity right now | Yes | Wait Retry-After seconds, then retry with backoff and jitter |
Error bodies#
detail is a list for 422. For 401, 402, 403, 429 and 529 it is an object with error_type and message. A 400 can take any of three shapes: an object with error_type and message, an object with only error_type (max_tokens_exceeded), or a plain string for a question's limits. Check its type before you branch on it.
A 422 lists every invalid field at once: loc is the path into your body (with the question's key and type), and input echoes what you sent there, so your own error message can point at the exact field.
{
"detail": [
{
"type": "missing",
"loc": ["body", "questions", "route", "choice", "criteria"],
"msg": "Field required",
"input": { "type": "choice", "instructions": "Where should this go?" }
}
]
}Over a limit: 400#
A request that parses but asks for more than a model allows is refused before any model work, and nothing is charged. The body says which limit you crossed, so log it as it is. The limits themselves are on Limits.
| Case | Body |
|---|---|
| More than 255 options | {"detail": "Too many choices. Must have at most 255 choices."} |
| More than 10 score levels | {"detail": "Too many score levels. Must have at most 10 levels."} |
| A number grid the first release does not serve (over 256 values, or a step below 1) | The api_usage_error shape, with a message that names the grid. Do not depend on the exact message wording |
| Over 64k tokens | {"detail": {"error_type": "max_tokens_exceeded"}} |
| Unknown model | {"detail": {"error_type": "api_usage_error", "message": "Unknown model: <name>"}} |
| Bad image | The api_usage_error shape, with a message that names the problem: undecodable, an unsupported type or over the size limit |
A number question needs min at or below max, and uses no criteria: its grid is the set of answers. Keep the grid within the supported limits.
Out of credit: 402#
Requests draw down a prepaid balance. When it reaches zero, every request returns HTTP 402 with {"detail": {"error_type": "insufficient_credit", "message": "..."}} until credit is added, so a runaway job cannot spend money you did not put in. A refused request is not billed and runs no model work.
Do not retry a 402 in a loop: it keeps failing until the balance has credit again. Turn on auto reload under Billing so the balance tops itself up before it runs out, and alert on any 402 your service still sees. Pricing and billing has the details.
Refused by the safety check: 403#
Every request passes an automatic safety check before the model answers. It refuses a narrow set of uses that the Acceptable Use Policy forbids: choosing whom to harm by a protected trait (race, religion, sex, disability and the others the policy lists), the lethal targeting of people, and help to self-harm. Detecting a risk is never refused: asking whether a message shows a risk of self-harm, or whether a post is a threat, is answered as usual.
A refused request returns HTTP 403 with {"detail": {"error_type": "safety_refusal", "message": "..."}} instead of answers. Nothing about it is worth retrying: the same request is refused again. A refused request is not charged (pending confirmation).
Retrying#
import random, time, requests
def decide(body, key, attempts=5):
for attempt in range(attempts):
r = requests.post(
"https://api.decisionnode.com/v1/decide",
json=body,
headers={"Authorization": f"Bearer {key}"},
timeout=10,
)
if r.status_code == 429:
time.sleep(float(r.headers.get("Retry-After", 1)))
continue
if r.status_code == 529:
# Retry-After is the server's estimate: wait at least that long
backoff = min(8, 0.25 * 2 ** attempt)
wait = max(float(r.headers.get("Retry-After", 0)), backoff)
time.sleep(wait + random.random() / 4)
continue
if not r.ok:
# 400, 401, 402, 403, 422: retrying will not help.
# (402 clears only once credit is added.)
# detail is a list, an object or a string: log it whole.
detail = r.json().get("detail")
raise RuntimeError(f"{r.status_code}: {detail!r}")
return r.json()
raise RuntimeError("DecisionNode: out of retries")