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

StatuscodeMeaningCharged?Retry?
400invalid_request_errorThe request shape is wrong. detail names the question and the expected shape.NoOnly after fixing it
400invalid_idempotency_keyIdempotency-Key is outside 8–255 of A-Za-z0-9._:-.NoAfter fixing
401invalid_api_keyMissing, unknown, revoked or wrong-type key. reason says which.NoAfter fixing
402insufficient_quotaNeither the token wallet nor a credit can cover the call.NoAfter topping up
404model_not_foundmodel is not jev-1.13 or jev-latest.NoAfter fixing
409request_in_progress · request_already_completed · idempotency_key_reusedAn Idempotency-Key collision.NoNo — read the original
413request_too_largeBody over 4 MB.NoAfter shrinking
422input_too_largeInput exceeds the 32,000-token context. Never truncated silently.NoAfter shortening or splitting
429rate_limit_exceededKey rate limit. Retry-After in seconds.NoYes, after the delay
429api_key_spend_limit_exceededKey daily decision budget. reset_at says when it clears.NoAfter the reset or a raised limit
499request_cancelledYour client disconnected or cancelled.NoYes
502upstream_unreachable · upstream_invalid_response · upstream_errorThe model could not be reached or answered unusably.NoYes, with backoff
503service_unavailableThe decision service is temporarily unavailable.NoYes, with backoff
504upstream_timeoutThe model did not answer in time.NoYes

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"