Before the first request
Iryx by Zyrex turns a context into typed decisions. You send what you know (a message, a record, a small state) and the questions you need answered. Each answer comes back with a value and the full probability spectrum behind it. Iryx does not write text.
This documentation describes API version 1 ("Espectro", after the spectrum every answer carries) as the service works today. All values in the examples are illustrative, not measurements.
- Access comes with a reservation: the API address and your key.
- In the examples, the address lives in the variable
IRYX_URLand the key inIRYX_KEY. - The key goes only in the header
Authorization: Bearer <key>, never in a URL or in the body. - The API speaks its own words:
context,decisions,pick,check,scale,spectrum.
export IRYX_URL="<the address that comes with your access>"
export IRYX_KEY="<your key>"$env:IRYX_URL = "<the address that comes with your access>"
$env:IRYX_KEY = "<your key>"[ part 1 ]
Cookbook
Short recipes for the most common uses. Every field and rule is in the API Reference.
Your first decision
A POST /v1/decide request carries the context and the decisions. This one carries three, one of each kind, about the same message.
curl -s "$IRYX_URL/v1/decide" \
-H "Authorization: Bearer $IRYX_KEY" \
-H "Content-Type: application/json" \
-d '{
"context": {"message": "API down since 9am, our checkout is failing"},
"decisions": {
"team": {"kind": "pick", "prompt": "Which team should handle this ticket?",
"options": {"billing": "charges, invoices, refunds", "tech": "bugs, API, outages"}},
"urgent": {"kind": "check", "prompt": "Needs a reply within hours"},
"mood": {"kind": "scale", "prompt": "Customer frustration",
"levels": ["calm", "annoyed", "angry"]}
}
}'PowerShell’s curl is another command; call curl.exe and keep the body in a file to avoid quoting problems.
curl.exe -s "$env:IRYX_URL/v1/decide" -H "Authorization: Bearer $env:IRYX_KEY" -H "Content-Type: application/json" --data-binary "@request.json"Response (illustrative values):
{"id": "dec_5b1e0c9a7d2f4e6a8c3b1d0f",
"object": "decision",
"model": "<model-id>",
"results": {
"team": {"kind": "pick", "value": "tech", "confidence": 0.95, "spectrum": {"billing": 0.05, "tech": 0.95}},
"urgent": {"kind": "check", "value": 0.91},
"mood": {"kind": "scale", "value": 1.4, "level": "annoyed", "spectrum": {"calm": 0.1, "annoyed": 0.4, "angry": 0.5}}},
"usage": {"decisions": 3},
"latency_ms": 120}resultshas one entry per decision, with the names from the request, in request order.usage.decisionscounts the decisions answered: it is the usage unit.ididentifies the response:dec_plus 24 hexadecimal characters, the same hex as theX-Request-Idheader.modelis the id of the model that answered, as listed byGET /v1/models.latency_msis the time the server spent on the request, in milliseconds, from receipt to answer. It includes any wait behind earlier requests; network time is not included.
pick, check and scale
Every decision has a kind, a prompt and, depending on the kind, options or levels. A decision is always about the context you send.
pick: choose one option
Use it when exactly one option of a fixed set applies. Descriptions are part of the question: say what each option covers.
"team": {"kind": "pick", "prompt": "Which team should handle this ticket?",
"options": {"billing": "charges, invoices, refunds", "tech": "bugs, API, outages", "sales": "prices, plans, upgrades"}}"team": {"kind": "pick", "value": "tech", "confidence": 0.88,
"spectrum": {"billing": 0.07, "tech": 0.88, "sales": 0.05}}value: the option with the highest probability. On a tie, the option listed first wins.confidence: the probability ofvalue(the same number asspectrum[value]).spectrum: every option with its probability, in request order.
check: is this statement true?
Write the prompt as a statement about the context that can be true or false ("Needs a reply within hours"), not as a question.
"urgent": {"kind": "check", "prompt": "Needs a reply within hours"}"urgent": {"kind": "check", "value": 0.91}value: the probability, from 0 to 1, that the statement is true. The probability that it is false is1 - value. There is nospectrum.
scale: where on an ordered scale?
List the levels from lowest to highest.
"mood": {"kind": "scale", "prompt": "Customer frustration", "levels": ["calm", "annoyed", "angry"]}"mood": {"kind": "scale", "value": 1.4, "level": "annoyed",
"spectrum": {"calm": 0.1, "annoyed": 0.4, "angry": 0.5}}spectrum: the probability of each level, by name, in request order.value: the expected position on the scale, counting the first level as 0 and the last asn - 1:0 × 0.1 + 1 × 0.4 + 2 × 0.5 = 1.4. Three decimals.level: the name of the level nearest tovalue(a fraction of exactly .5 rounds up). Here 1.4 rounds to 1,"annoyed".
Questions about this service itself are not accepted: a decision whose prompt, options or levels ask about the service returns 422 self_reference_not_supported.
Several decisions in one request
One request carries 1 to 32 decisions about the same context (a server may be configured with a lower limit). Each decision answered counts one in usage.decisions. Several contexts in one request are not in v1: send one request per context.
In Python, standard library only, handling errors by their type:
import json
import os
import urllib.error
import urllib.request
URL = os.environ["IRYX_URL"] + "/v1/decide"
KEY = os.environ.get("IRYX_KEY") # your key
body = {
"context": {"message": "API down since 9am, our checkout is failing"},
"decisions": {
"team": {"kind": "pick", "prompt": "Which team should handle this ticket?",
"options": {"billing": "charges, invoices, refunds", "tech": "bugs, API, outages"}},
"urgent": {"kind": "check", "prompt": "Needs a reply within hours"},
},
}
headers = {"Content-Type": "application/json"}
if KEY:
headers["Authorization"] = "Bearer " + KEY
request = urllib.request.Request(URL, data=json.dumps(body).encode("utf-8"), headers=headers, method="POST")
try:
with urllib.request.urlopen(request, timeout=60) as response:
answer = json.load(response)
except urllib.error.HTTPError as error:
problem = json.load(error)
raise SystemExit("%d %s: %s (param=%s, request id %s)" % (
error.code, problem["error"]["type"], problem["error"]["message"],
problem["error"].get("param"), problem["id"]))
team = answer["results"]["team"]
print(team["value"], team["confidence"], answer["results"]["urgent"]["value"])How much fits in one request
Every decision is read together with the whole context. The longer the context and the more decisions in one request, the sooner the limit comes. For ordinary prose, with a short prompt and a few short options per decision, the context fits in about:
| decisions in the request | context, about |
|---|---|
| 1 to 4 | 6,000 characters |
| 8 | 3,000 characters |
| 16 | 1,300 characters |
| 32 | 500 characters |
- Over the limit: you get
422 context_too_longand nothing was decided. Send the same context with fewer decisions per request, or a shorter context. - Server at capacity: you get
503 overloadedwith theRetry-Afterheader. Wait that many seconds and send the same request again; nothing was decided. - A structured context works: keys and values are part of what Iryx reads, so clear key names help.
Reading confidence and the spectrum
Each kind tells you how far to trust the answer in its own way:
pick:confidenceis the probability of the chosen option, and thespectrumshows the others, including the runner-up.check:valueis the probability that the statement is true. For a yes/no reading, compare it with 0.5, or with a stricter threshold when a wrong "yes" is costly.scale:valuekeeps the uncertainty (1.4 is between "annoyed" and "angry", closer to "annoyed");levelis the nearest named level, for display or routing.
"team": {"kind": "pick", "value": "tech", "confidence": 0.88,
"spectrum": {"billing": 0.07, "tech": 0.88, "sales": 0.05}}Here tech comes out at 0.88 and the runner-up is billing, at 0.07. The numbers can move slightly between two identical requests: see Reading the numbers.
Handling the low band
A response doesn’t come with a ready-made band: it comes with the probability. You set the low band: below a threshold you choose, the decision goes to a person.
- Choose a threshold for each decision. Above it, your software acts on its own; below it, a person decides.
- Send the spectrum along to that person: it shows the runner-up.
- In a
check, use a threshold stricter than 0.5 when a wrong "yes" is costly. - An almost tied answer can switch sides between two identical requests: one more reason for a person to check.
import os
import requests
URL = os.environ["IRYX_URL"] + "/v1/decide"
headers = {"Authorization": "Bearer " + os.environ["IRYX_KEY"]} if os.environ.get("IRYX_KEY") else {}
body = {
"context": {"temperature_c": 31, "co2_ppm": 1800, "someone_home": True},
"decisions": {
"action": {"kind": "pick", "prompt": "What should the home system do now?",
"options": ["nothing", "turn_on_ventilation", "alert_resident", "call_emergency"]},
"severity": {"kind": "scale", "prompt": "How serious is the situation?",
"levels": ["normal", "attention", "serious", "emergency"]},
},
}
response = requests.post(URL, json=body, headers=headers, timeout=60)
if response.status_code != 200:
problem = response.json()
raise SystemExit(f"{response.status_code} {problem['error']['type']}: {problem['error']['message']}")
result = response.json()["results"]
action = result["action"]
if action["confidence"] >= 0.8: # illustrative threshold: pick your own
print("do it:", action["value"])
else:
print("ask a person; spectrum:", action["spectrum"])
print("severity:", result["severity"]["level"], result["severity"]["value"])The 0.8 threshold in the example is illustrative: pick your own. requests sends Content-Type: application/json by itself when you pass json=.
[ part 2 ]
API Reference
Every field, limit, error and header of API v1, as the service works today. All values in the examples are illustrative.
Overview
| Protocol | HTTP/1.1, JSON bodies in UTF-8 |
|---|---|
| Base URL | comes with your access (IRYX_URL in the examples) |
| Version | path prefix /v1; GET /v1/health reports "api_version": "1" |
| Authentication | Authorization: Bearer <key> |
| Usage unit | the decision: every response reports usage.decisions |
| method | path | what it does | key needed |
|---|---|---|---|
POST | /v1/decide | answers 1 to 32 decisions about one context | yes |
GET | /v1/models | lists the model ids this server answers with | yes |
GET | /v1/health | liveness and readiness | no |
GET | /health | same as /v1/health | no |
OPTIONS | any path | CORS preflight | no |
Authentication
- Every request carries
Authorization: Bearer <key>, exceptGET /v1/health,GET /healthandOPTIONS. The wordBeareris case-insensitive. - A missing or wrong key returns
401 unauthorizedwith the headerWWW-Authenticate: Bearer. Without a valid key, an unknown path also returns 401, not 404. - Send the key only in the header, never in a URL or in the body.
POST /v1/decide
Answers 1 to 32 decisions about one context. A full example is in Your first decision.
Request body
| field | type | required | rules |
|---|---|---|---|
model | string | no | a model id from GET /v1/models, or an alias the server accepts for one of them (see GET /v1/models). Omitted: the server’s default model (the one /v1/health reports). Unknown id: 404 model_not_found |
context | object | yes | any JSON object, up to 65,536 bytes when serialized as compact UTF-8 JSON. For a single piece of free text, use {"message": "<text>"} |
decisions | object | yes | 1 to 32 entries (a server may be configured with a lower limit): decision name → decision object. Names match ^[A-Za-z0-9_.-]{1,64}$ |
Any other top-level field returns 422 invalid_request, with param set to that field. The whole body is limited to 1,048,576 bytes by default (413 above it).
The context is read as given. A structured context works: keys and values are part of what Iryx reads, so clear key names help the reader ({"temperature_c": 31, "co2_ppm": 1800} rather than {"t": 31, "c": 1800}).
Decision object
| field | kinds | type | required | rules |
|---|---|---|---|---|
kind | all | string | yes | pick, check or scale |
prompt | all | string | yes | not empty or blank, up to 2,000 characters |
options | pick | object or array | yes | 2 to 26 options. Object: name → description (a string, "" allowed, up to 2,000 characters). Array: names only. Names are non-blank strings, up to 200 characters, unique |
levels | scale | array | yes | 2 to 26 level names, ordered from lowest to highest. Non-blank, up to 200 characters, unique |
A field that does not belong to the kind (for example levels in a pick) returns 422 invalid_request with param = decisions.<name>.<field>.
Decisions are about the context you send. Questions about this service itself are not accepted: a decision whose prompt, options or levels ask about the service returns 422 self_reference_not_supported.
Rules for each kind
pick:valueis the option with the highest probability; on a tie, the option listed first wins.confidenceis the probability ofvalue(the same number asspectrum[value]).spectrumhas every option, in request order.check: write thepromptas a statement, not as a question.valueis the probability, from 0 to 1, that the statement is true; the probability that it is false is1 - value. There is nospectrum.scale: levels go from lowest to highest.spectrumhas the probability of each level, in request order.valueis the expected position, counting the first level as 0 and the last asn - 1, with three decimals.levelis the name of the level nearest tovalue(a fraction of exactly .5 rounds up).
Examples of each kind are in pick, check and scale.
Response body (200)
| field | type | meaning |
|---|---|---|
id | string | dec_ + 24 hexadecimal characters. Same hex as the X-Request-Id header (req_...) |
object | string | always "decision" |
model | string | the model id that answered, as listed by GET /v1/models. A request that named an alias gets the listed id here, not the alias |
results | object | one entry per decision, with the names from the request, in request order |
usage | object | {"decisions": n}: how many decisions were answered |
latency_ms | integer | time the server spent on the request, in milliseconds, from receipt to answer. It includes any wait behind earlier requests; network time is not included |
Result object, by kind:
| field | pick | check | scale |
|---|---|---|---|
kind | "pick" | "check" | "scale" |
value | string: the chosen option | number from 0 to 1 | number from 0 to n - 1 |
confidence | number from 0 to 1 | not present | not present |
level | not present | not present | string: nearest level |
spectrum | object: option → probability | not present | object: level → probability |
Reading the numbers
- Probabilities have 4 decimals and lie between 0 and 1. A spectrum sums to 1 up to rounding, and a small probability can round to 0.
- Answers do not depend on random sampling, but they are not repeatable to the last decimal: the same request sent twice can come back with slightly different probabilities, by up to about 0.02 (the same goes for
valuein acheckor ascale). The chosen option of apick, thelevelof ascaleand the side of 0.5 in acheckstay the same unless the answer is almost tied. - Use
confidence(pick) andvalue(check) as probabilities. A common pattern: act automatically above a threshold you choose, and send the rest to a person. The spectrum shows the runner-up. - For a yes/no reading of a
check, comparevaluewith 0.5, or with a stricter threshold when a wrong "yes" is costly. - In a
scale,valuekeeps the uncertainty (1.4 is between "annoyed" and "angry", closer to "annoyed");levelis the nearest named level, for display or routing.
GET /v1/models
{"data": [{"id": "<model-id>", "object": "model"}]}Lists the model ids this server answers with. Any of them can go in the model field of /v1/decide.
A server may also accept an alias: another id that answers exactly as one of the listed models, for example an older id kept for clients that still send it. Aliases are not listed here. A request with an alias is answered by the model it points to, and the response carries the listed id. An alias that the server does not know is 404 model_not_found, like any unknown id.
GET /v1/health and /health
{"status": "ok", "model": "<model-id>", "api_version": "1"}modelis the server’s default model, used when a request omitsmodel.- No key is needed.
- The server starts listening only after the model is loaded, so a 200 here means the service is ready.
OPTIONS CORS preflight
Any path returns 204 without a key, with Access-Control-Allow-Methods: GET, POST, OPTIONS, Access-Control-Allow-Headers: Authorization, Content-Type and Access-Control-Max-Age: 600. The allowed origin is set by whoever operates the service; by default there is none, so a browser page from another origin cannot read the answers.
Errors
Every error is JSON, with the same shape:
{"error": {"type": "invalid_request",
"message": "\"options\" must have between 2 and 26 items.",
"param": "decisions.team.options"},
"id": "req_5b1e0c9a7d2f4e6a8c3b1d0f"}type: stable and machine-readable. Branch on it (and onparam), not onmessage.message: English, for people. It may change.param: present when one field caused the error.id: the request id, equal to theX-Request-Idheader. Quote it when reporting a problem.
| status | type | when |
|---|---|---|
| 400 | invalid_json | the body is empty, is not valid UTF-8 JSON, starts with a byte order mark, uses NaN or Infinity, is nested too deep, has a number out of range (1e999) or an unpaired surrogate escape ("\ud800"), is shorter than Content-Length, is sent with chunked transfer encoding, has an invalid Content-Length, or takes too long to arrive |
| 400, 414, 431, 505 | bad_request | the HTTP request itself is malformed: bad request line (400), URL too long (414), too many or too large headers (431), unsupported HTTP version (505) |
| 401 | unauthorized | the request has no valid Authorization: Bearer <key> |
| 404 | not_found | unknown path |
| 404 | model_not_found | model is not one of the ids in GET /v1/models (param: model) |
| 405 | method_not_allowed | wrong method for a known path (the Allow header lists the right ones), or a method the server does not know at all (no Allow header) |
| 413 | payload_too_large | the body is larger than the server’s limit (1,048,576 bytes by default). The message states the limit |
| 422 | invalid_request | valid JSON that breaks a rule of this reference: missing or unknown field, wrong type, a limit exceeded, repeated option or level names |
| 422 | self_reference_not_supported | a decision asks about this service itself instead of about the context (param: the prompt, options or levels of that decision). Nothing was decided |
| 422 | context_too_long | the request is valid, but the context and the decisions are more than the model can read at once (see Limits). param is always context and the message is always context too long for this model. Nothing was decided: shorten the context or split the decisions into several requests |
| 500 | internal_error | an unexpected failure. The message is generic; the request id lets whoever operates the service find it |
| 503 | overloaded | the server is at capacity and could not answer this request now. The Retry-After header says how many seconds to wait before trying again. Nothing was decided; the request can be repeated as is |
Specific 400 messages
| message |
|---|
Request body is empty. Send a JSON object. |
Request body is not valid JSON. |
Numbers must be finite (a number in the body is out of range). |
Request body has an unpaired surrogate escape (invalid Unicode). |
Send the body with a Content-Length header (chunked transfer is not supported). |
Invalid Content-Length header. |
Request body ended before Content-Length bytes. |
Timed out reading the request body. |
Forms of param
param | points to |
|---|---|
model | the model field |
context | the context field (missing, not an object, too large), and every context_too_long |
decisions | the decisions field, or an invalid decision name |
decisions.team.kind, decisions.team.prompt | a field of one decision |
decisions.team.options, decisions.mood.levels | the whole list or object |
decisions.team.options[1], decisions.mood.levels[0] | one item of a list |
decisions.team.options.billing | the description of one option |
| any other name | an unknown top-level field |
Headers
Send:
| header | when |
|---|---|
Content-Type: application/json | on every POST |
Content-Length | on every request with a body. Chunked transfer encoding is not supported |
Authorization: Bearer <key> | on every request that needs a key (see Authentication) |
Every JSON response (all statuses except the 204 of OPTIONS) carries:
| header | value |
|---|---|
Content-Type | application/json; charset=utf-8 |
X-Request-Id | req_ + 24 hexadecimal characters |
Cache-Control | no-store |
X-Content-Type-Options | nosniff |
Server | iryx |
Access-Control-Allow-Origin | the allowed origin, when CORS is on (with Access-Control-Expose-Headers: X-Request-Id, Retry-After, and Vary: Origin when the origin is not *) |
WWW-Authenticate: Bearer | on 401 |
Allow | on 405 for a known path |
Retry-After | on 503: seconds to wait before retrying |
The 204 answer to OPTIONS has no body: it carries X-Request-Id, Server, Allow and the CORS headers above.
Connections are HTTP/1.1 keep-alive. The server closes the connection after an error that left part of the body unread.
Limits
| what | limit |
|---|---|
| request body | 1,048,576 bytes by default |
context | 65,536 bytes, serialized as compact UTF-8 JSON |
| decisions per request | 1 to 32 |
| decision name | 1 to 64 characters from A-Z a-z 0-9 _ . - |
prompt | 1 to 2,000 characters |
| options (pick), levels (scale) | 2 to 26 |
| option or level name | 1 to 200 characters |
| option description | 0 to 2,000 characters |
| silence on a connection | 60 s: a connection that sends nothing for 60 s is closed (in the middle of a body, with 400 invalid_json) |
| what the model reads at once | see below; above it, 422 context_too_long |
How much the model reads at once
Every decision is read together with the whole context. The longer the context and the more decisions in one request, the sooner the limit comes. For ordinary prose, with a short prompt and a few short options per decision, the context fits in about:
| decisions in the request | context, about |
|---|---|
| 1 to 4 | 6,000 characters |
| 8 | 3,000 characters |
| 16 | 1,300 characters |
| 32 | 500 characters |
These numbers are approximate on purpose. Long prompts and option descriptions count too, and text that is not ordinary prose (numbers, code, JSON with many short keys, emoji, some non-Latin scripts) reaches the limit sooner. If you get context_too_long, send the same context with fewer decisions per request, or a shorter context. The context byte limit in the table above is a separate, larger ceiling on the request itself.
Requests that arrive while the server is busy wait for their turn. The waiting time shows in latency_ms. When the server is at capacity it answers 503 overloaded with Retry-After; wait that many seconds and send the same request again. There are no rate-limit headers in v1.
Versioning
- The API version is in the path (
/v1) and inGET /v1/health("api_version": "1"). A change that would break v1 clients goes to a new path. - Within v1, responses may gain new optional fields. Clients should ignore fields they do not know.
- Model ids are fixed: one id always names the same model, and so does each alias of it. A new model gets a new id, listed by
GET /v1/models. To follow the server’s default instead of pinning a model, omitmodel.
Data policy
- The server does not store what you send or what it answers. It reads the request, decides in memory and replies. Because content is not kept, it is not reused for anything.
- The server log has one line per request: time, request id, method, route, status, number of decisions and duration. Never the context, prompts, options or results.
- An unexpected error adds its error type and the code location to the log, without the error message, which could echo client content.
- Responses carry
Cache-Control: no-store.
The hosted service isn’t live yet; its rules will be in the terms of use before the first customer.
Not in v1
- Text generation, streaming, or several contexts in one request (send one request per context).
- Idempotency keys, rate-limit headers and chunked uploads.
- Any field that says how an answer was produced: a response carries the decision, its probabilities and the usage count, nothing else.