API Reference
System One
POST /v1/systemone — typed choice, score and noul decisions
System One answers typed decision questions — choice, score, and noul — in a single JSON response. Decision models are discovered like any other model; see Models.
Endpoint:
POST /v1/systemoneAuth: Bearer token or x-api-key. Requires scope inference.decisions; listing models with Models needs inference.models as well (inference.* covers both).
Example
import httpx
response = httpx.post(
"https://api.melious.ai/v1/systemone",
headers={"Authorization": "Bearer sk-mel-<YOUR_API_KEY>"},
json={
"model": "<DECISION_MODEL_ID>",
"state": "Ticket #4128: customer requests a refund 11 days after purchase; the order has shipped.",
"questions": {
"resolution": {
"type": "choice",
"instructions": "Pick the support resolution that matches policy.",
"criteria": {
"refund": "Refund in full",
"credit": "Store credit",
"deny": "Deny politely",
},
}
},
},
timeout=60.0, # httpx defaults to 5 s; a decision can take longer
)
response.raise_for_status()
print(response.json()["answers"]["resolution"]["choice"])curl https://api.melious.ai/v1/systemone \
-H "Authorization: Bearer sk-mel-<YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"model": "<DECISION_MODEL_ID>",
"state": "Ticket #4128: customer requests a refund 11 days after purchase; the order has shipped.",
"questions": {
"resolution": {
"type": "choice",
"instructions": "Pick the support resolution that matches policy.",
"criteria": {
"refund": "Refund in full",
"credit": "Store credit",
"deny": "Deny politely"
}
}
}
}'Set a client timeout you can live with — the server allows a decision up to 20 minutes, and httpx gives up after 5 seconds by default.
The response:
{
"model": "<DECISION_MODEL_ID>",
"answers": {
"resolution": {
"type": "choice",
"choice": "credit",
"probabilities": { "refund": 0.25, "credit": 0.7, "deny": 0.05 },
"confidence": 0.7
}
},
"usage": { "input_tokens": 214 }
}Availability
Contact support@melious.ai to confirm System One availability. When the endpoint is unavailable, requests with a valid key that has inference.decisions return 404 not_found. A key without that scope gets 403 auth_insufficient_scope first, whether or not the endpoint is on.
Request
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | yes | Decision model id or alias. The response's model is the canonical catalog id. |
state | string, object, or array | yes | The context the questions are decided against. May be empty. |
questions | object | yes | 1–32 named questions, each with a type and instructions. choice and score require criteria; noul doesn't. |
images | array | no | A non-empty array returns 422 invalid_request unless the model's _meta.capabilities.decision.images is true. |
state and each question's instructions accept strings (including empty), objects, or arrays.
Choice questions
criteria is an object of 2–255 options. Keys are labels of at most 128 characters; values can be any JSON, including null. The answer picks one label and reports probabilities keyed by option labels, together summing to 1; not every option necessarily appears.
Score questions
criteria is an ordered array of 2–10 levels; values can be any JSON. The answer's score is the probability-weighted mean of the level positions — 0.1 × 0 + 0.8 × 1 + 0.1 × 2 = 1.0 in the example below — so it's a decimal between 0 and the last position, not always a whole level. legend maps each position back to the level you supplied.
Noul questions
A noul question has no options. The answer is one number in [0, 1]. criteria is optional. When you send it, it's an object with true and/or false — each can be any JSON, and either one can be left out.
Limits
These are the service-wide maxima; a model can set lower limits.
- 1–32 questions per request. Question names are 1–100 characters: ASCII letters, digits, dots, underscores, hyphens.
- Choice questions: 2–255 options, option names at most 128 characters.
- Score questions: 2–10 levels.
- Model names: at most 128 characters.
- Request bodies: at most 262,144 bytes (256 KiB) as sent, whitespace included. Larger bodies return
413 request_too_large.
Per-model limits are exposed as an object under _meta.capabilities.decision: max_questions, max_choice_options, max_score_levels, max_request_bytes, and images (a boolean — false means a non-empty images array returns 422 invalid_request). _meta.context_length is set for decision models too. Exceeding a model's limit returns 422 invalid_request — a lower model byte limit returns 413 request_too_large.
Find decision models with Models: request ?include_meta=true and filter _meta.type to decision.
Response
Success is HTTP 200 with one JSON body containing exactly model, answers, and usage. There is no streaming mode and no stream parameter.
A request using all three question types:
{
"model": "<DECISION_MODEL_ID>",
"state": "Ticket #4128: customer requests a refund 11 days after purchase; the order has shipped.",
"questions": {
"resolution": {
"type": "choice",
"instructions": "Pick the support resolution that matches policy.",
"criteria": {
"refund": "Refund in full",
"credit": "Store credit",
"deny": "Deny politely"
}
},
"urgency": {
"type": "score",
"instructions": "How urgent is this on the ordered scale?",
"criteria": ["routine", "same-day", "immediate"]
},
"escalate": {
"type": "noul",
"instructions": "Should a human review this before closing?"
}
}
}Its response:
{
"model": "<DECISION_MODEL_ID>",
"answers": {
"resolution": {
"type": "choice",
"choice": "credit",
"probabilities": { "refund": 0.25, "credit": 0.7, "deny": 0.05 },
"confidence": 0.7
},
"urgency": {
"type": "score",
"score": 1.0,
"probabilities": { "0": 0.1, "1": 0.8, "2": 0.1 },
"confidence": 0.8,
"legend": { "0": "routine", "1": "same-day", "2": "immediate" }
},
"escalate": {
"type": "noul",
"noul": 0.83
}
},
"usage": { "input_tokens": 214 }
}Answers are keyed by your question names:
- choice —
choiceis the selected label from your criteria;probabilitiesgives each option it reports a share in[0, 1], together summing to 1;confidenceaccompanies them. - score —
scoreis the probability-weighted mean position on your ordered scale (a decimal);probabilitiesis keyed by level position ("0","1", …) with the same constraints;legendmaps positions back to your levels. - noul —
noulis one number in[0, 1].
usage carries input_tokens and output_tokens when measured. 0 is a measured zero; an omitted field means the count couldn't be measured.
Errors
This endpoint has its own error envelope. Every error body contains a lowercase code, a message, request_id, type, and param:
{
"error": {
"code": "unsupported_model",
"message": "The model does not support decisions.",
"request_id": "9f8c2a47-3d1b-4e6a-8b2f-1c9d5e7a0b3d",
"type": "invalid_request_error",
"param": null
},
"request_id": "9f8c2a47-3d1b-4e6a-8b2f-1c9d5e7a0b3d"
}Branch on code — it's fixed per cause and repeated in the X-System-One-Error-Code header; one X-Request-ID matches both request_id locations. type is a coarse OpenAI-style category and the same code can arrive with different type values, so don't classify on it. param is null or names the offending part: json, body, model, questions, questions.criteria, images, or Idempotency-Key.
| Status | Code | When |
|---|---|---|
| 400 | invalid_json | Body isn't valid JSON |
| 401 | invalid_api_key | Missing or invalid API key |
| 402 | insufficient_credits | Balance can't cover the reservation (see Billing), or cycle limits |
| 403 | auth_insufficient_scope | Key lacks inference.decisions |
| 403 | model_not_allowed | Key can't use the model |
| 403 | inference_model_restricted | Model restricted by your organization |
| 404 | not_found | Endpoint not found |
| 409 | request_in_progress | Same idempotency key still running (Retry-After: 2) |
| 409 | idempotency_conflict | Same key, different request |
| 409 | idempotency_failed | Original request failed |
| 410 | idempotency_expired | Replay window expired |
| 413 | request_too_large | Transport or model byte limit exceeded |
| 422 | invalid_request | Schema or limit violation, including images |
| 422 | invalid_idempotency_key | Malformed Idempotency-Key |
| 422 | unsupported_model | Unknown model id, not a decision model, or :free |
| 429 | rate_limit_exceeded | Rate limit hit |
| 500 | internal_error | Internal failure |
| 502 | invalid_provider_response | Provider returned an invalid answer |
| 503 | service_unavailable | Provider unavailable or overloaded |
| 503 | dedicated_capacity_unavailable | Your dedicated capacity is down or full |
| 503 | entitlement_lookup_failed | Capacity entitlements couldn't be read; retry |
| 503 | model_allowlist_lookup_failed | Key model permissions couldn't be read; retry |
| 504 | provider_timeout | Provider timed out |
429 and 503 responses carry Retry-After — follow the delay it names. 409 request_in_progress carries Retry-After: 2. General retry guidance: When to retry.
If you sent an Idempotency-Key and got a 502, 503, or 504, retry with a new key: the failed attempt marks the old key as failed, and reusing it returns 409 idempotency_failed. Nothing was charged for the failed attempt. After a 500, retry with the same key — if the answer was already produced, you get it back without a second charge.
Idempotency
Idempotency-Key is optional — an ASCII string of 1–128 characters that starts with a letter or digit, followed by letters, digits, dots, underscores, colons, or hyphens. With a key, a retry with the same key and the same request returns the stored response, with X-Idempotency-Replayed: true, instead of running — and billing — the request again. The key is scoped to your user account.
"Same request" means the same JSON body: key order, whitespace, and 1 versus 1.0 don't matter, but the model string does — an alias and its canonical id count as different requests and return 409 idempotency_conflict.
Replay is available for up to 24 hours from when the original request started. After that window, reusing a key can start a new request that may be billed. If you receive 410 idempotency_expired, use a new key for a new request.
Requests without a key are treated as new requests.
Conflicts:
409 request_in_progress— the same key is still running.409 idempotency_conflict— the same key with a different request.409 idempotency_failed— the original request failed with a502,503, or504; use a new key.
Billing
Decision models bill input tokens only — there is no output price. The charge is usage.input_tokens times the input rate of the provider that served the request. _meta.pricing.input_cost_per_million_eur on Models is the cheapest active provider's rate; routing prefers it, but a failover to another provider is billed at that provider's rate.
Concurrency plans do not cover the per-token cost. When the input-token count can't be measured, usage.input_tokens is omitted and the request is served without an input-token charge.
Before the request runs, Melious reserves the model's worst case — max_request_bytes tokens at the highest provider rate — and settles to the real charge afterwards. A 402 insufficient_credits means your available balance (balance minus other open reservations) is below that worst case, even for a small request. Top up, or pick a decision model with a lower max_request_bytes.
Decision responses carry no billing_cost or environment_impact — the body is model, answers, and usage. For how your plan pays for API requests, see Pricing.
Related
Decision model discovery: Models • Scopes: Authentication • Error handling: Errors