Errors
Every status POST /api/v1/systemone returns, what each one means, whether it was charged, and whether retrying will help.
Errors use one envelope:
{
"error": {
"message": "Human-readable summary.",
"type": "invalid_request_error",
"code": "invalid_request_error",
"param": null,
"detail": "Present when the problem is in your request and we can name it."
}
}
code is what you branch on. message is what you log. detail appears only
when the fix is in your request — a malformed criteria, an unknown model —
and never carries anything about our infrastructure.
Status Reference
| Status | code | Meaning | Charged? | Retry? |
|---|---|---|---|---|
400 | invalid_request_error | The request shape is wrong. detail names the question and the expected shape. | No | Only after fixing it |
400 | invalid_idempotency_key | Idempotency-Key is outside 8–255 of A-Za-z0-9._:-. | No | After fixing |
401 | invalid_api_key | Missing, unknown, revoked or wrong-type key. reason says which. | No | After fixing |
402 | insufficient_quota | Neither the token wallet nor a credit can cover the call. | No | After topping up |
404 | model_not_found | model is not jev-1.13 or jev-latest. | No | After fixing |
409 | request_in_progress · request_already_completed · idempotency_key_reused | An Idempotency-Key collision. | No | No — read the original |
413 | request_too_large | Body over 4 MB. | No | After shrinking |
422 | input_too_large | Input exceeds the 32,000-token context. Never truncated silently. | No | After shortening or splitting |
429 | rate_limit_exceeded | Key rate limit. Retry-After in seconds. | No | Yes, after the delay |
429 | api_key_spend_limit_exceeded | Key daily decision budget. reset_at says when it clears. | No | After the reset or a raised limit |
499 | request_cancelled | Your client disconnected or cancelled. | No | Yes |
502 | upstream_unreachable · upstream_invalid_response · upstream_error | The model could not be reached or answered unusably. | No | Yes, with backoff |
503 | service_unavailable | The decision service is temporarily unavailable. | No | Yes, with backoff |
504 | upstream_timeout | The model did not answer in time. | No | Yes |
Nothing here is ever charged
Every row says "No" and that is the whole rule: a decision is charged only
when it returns 200. Any hold placed before the failure is released
automatically — see Billing.
The Ones Worth Handling Explicitly
400 with a detail
This is almost always the criteria container. detail reads like:
questions.q.criteria: `criteria` is an array, which upstream rejects with 400.
A `choice` question takes `criteria` as an object mapping each label to a short
description, e.g. {"billing": "Invoices and refunds"}. Only `score` takes an
array.
It names the question id, so you can fix it without bisecting the payload. See Question types.
422 versus 413
413 means the HTTP body was too big. 422 means the body was fine but the
content did not fit the model's 32,000-token context. Splitting the state
fixes 422; compressing the JSON does not.
503 is ours, not yours
A 503 from this endpoint always means the decision service is temporarily
unavailable on our side. It never means your key or your balance is the
problem — those are 401 and 402, and they say so. Retry with backoff.
Retrying Safely
Retryable statuses are 429, 499, 502, 503 and 504. Because failures
are never charged, a retry costs nothing extra — but a retry after a timeout
can, in principle, duplicate work your own system does with the answer. Send an
Idempotency-Key and the second attempt cannot produce a second decision; see
Decisions.
const RETRYABLE = new Set([429, 499, 502, 503, 504]);
async function decide(body: unknown, attempt = 0): Promise<Response> {
const response = await fetch('https://jev-ai.org/api/v1/systemone/', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.JEV_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKeyFor(body),
},
body: JSON.stringify(body),
});
if (response.ok || !RETRYABLE.has(response.status) || attempt >= 3) {
return response;
}
const retryAfter = Number(response.headers.get('retry-after'));
const delay = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, delay));
return decide(body, attempt + 1);
}
Looking a Request Up Afterwards
Every response — success or failure — carries X-Request-ID. Give that id to
support, or read it yourself:
curl https://jev-ai.org/api/v1/requests/dec_2408.../ \
-H "Authorization: Bearer $JEV_API_KEY"