ZYREXdocs

[ docs · Iryx API v1 ]

Documentation

Everything you need to send a context and get typed decisions back from Iryx by Zyrex: short recipes in the Cookbook, and every field, limit and error in the API Reference.

[ access ]

The API isn’t open to the public yet. Access comes with a reservation: you get the API address and your key.

Reserve ↗
On this page

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_URL and the key in IRYX_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.
bash
export IRYX_URL="<the address that comes with your access>"
export IRYX_KEY="<your key>"
PowerShell
$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.

bash
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.

PowerShell
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):

json
{"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}
  • results has one entry per decision, with the names from the request, in request order.
  • usage.decisions counts the decisions answered: it is the usage unit.
  • id identifies the response: dec_ plus 24 hexadecimal characters, the same hex as the X-Request-Id header.
  • model is the id of the model that answered, as listed by GET /v1/models.
  • latency_ms is 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.

json
"team": {"kind": "pick", "prompt": "Which team should handle this ticket?",
         "options": {"billing": "charges, invoices, refunds", "tech": "bugs, API, outages", "sales": "prices, plans, upgrades"}}
json
"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 of value (the same number as spectrum[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.

json
"urgent": {"kind": "check", "prompt": "Needs a reply within hours"}
json
"urgent": {"kind": "check", "value": 0.91}
  • value: the probability, from 0 to 1, that the statement is true. The probability that it is false is 1 - value. There is no spectrum.

scale: where on an ordered scale?

List the levels from lowest to highest.

json
"mood": {"kind": "scale", "prompt": "Customer frustration", "levels": ["calm", "annoyed", "angry"]}
json
"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 as n - 1: 0 × 0.1 + 1 × 0.4 + 2 × 0.5 = 1.4. Three decimals.
  • level: the name of the level nearest to value (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:

Python
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 requestcontext, about
1 to 46,000 characters
83,000 characters
161,300 characters
32500 characters
  • Over the limit: you get 422 context_too_long and nothing was decided. Send the same context with fewer decisions per request, or a shorter context.
  • Server at capacity: you get 503 overloaded with the Retry-After header. 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: confidence is the probability of the chosen option, and the spectrum shows the others, including the runner-up.
  • check: value is 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: value keeps the uncertainty (1.4 is between "annoyed" and "angry", closer to "annoyed"); level is the nearest named level, for display or routing.
json
"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.

  1. Choose a threshold for each decision. Above it, your software acts on its own; below it, a person decides.
  2. Send the spectrum along to that person: it shows the runner-up.
  3. In a check, use a threshold stricter than 0.5 when a wrong "yes" is costly.
  4. An almost tied answer can switch sides between two identical requests: one more reason for a person to check.
Python
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

ProtocolHTTP/1.1, JSON bodies in UTF-8
Base URLcomes with your access (IRYX_URL in the examples)
Versionpath prefix /v1; GET /v1/health reports "api_version": "1"
AuthenticationAuthorization: Bearer <key>
Usage unitthe decision: every response reports usage.decisions
methodpathwhat it doeskey needed
POST/v1/decideanswers 1 to 32 decisions about one contextyes
GET/v1/modelslists the model ids this server answers withyes
GET/v1/healthliveness and readinessno
GET/healthsame as /v1/healthno
OPTIONSany pathCORS preflightno

Authentication

  • Every request carries Authorization: Bearer <key>, except GET /v1/health, GET /health and OPTIONS. The word Bearer is case-insensitive.
  • A missing or wrong key returns 401 unauthorized with the header WWW-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

fieldtyperequiredrules
modelstringnoa 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
contextobjectyesany JSON object, up to 65,536 bytes when serialized as compact UTF-8 JSON. For a single piece of free text, use {"message": "<text>"}
decisionsobjectyes1 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

fieldkindstyperequiredrules
kindallstringyespick, check or scale
promptallstringyesnot empty or blank, up to 2,000 characters
optionspickobject or arrayyes2 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
levelsscalearrayyes2 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: value is the option with the highest probability; on a tie, the option listed first wins. confidence is the probability of value (the same number as spectrum[value]). spectrum has every option, in request order.
  • check: write the prompt as a statement, not as a question. value is the probability, from 0 to 1, that the statement is true; the probability that it is false is 1 - value. There is no spectrum.
  • scale: levels go from lowest to highest. spectrum has the probability of each level, in request order. value is the expected position, counting the first level as 0 and the last as n - 1, with three decimals. level is the name of the level nearest to value (a fraction of exactly .5 rounds up).

Examples of each kind are in pick, check and scale.

Response body (200)

fieldtypemeaning
idstringdec_ + 24 hexadecimal characters. Same hex as the X-Request-Id header (req_...)
objectstringalways "decision"
modelstringthe model id that answered, as listed by GET /v1/models. A request that named an alias gets the listed id here, not the alias
resultsobjectone entry per decision, with the names from the request, in request order
usageobject{"decisions": n}: how many decisions were answered
latency_msintegertime 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:

fieldpickcheckscale
kind"pick""check""scale"
valuestring: the chosen optionnumber from 0 to 1number from 0 to n - 1
confidencenumber from 0 to 1not presentnot present
levelnot presentnot presentstring: nearest level
spectrumobject: option → probabilitynot presentobject: 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 value in a check or a scale). The chosen option of a pick, the level of a scale and the side of 0.5 in a check stay the same unless the answer is almost tied.
  • Use confidence (pick) and value (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, compare value with 0.5, or with a stricter threshold when a wrong "yes" is costly.
  • In a scale, value keeps the uncertainty (1.4 is between "annoyed" and "angry", closer to "annoyed"); level is the nearest named level, for display or routing.

GET /v1/models

json
{"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

json
{"status": "ok", "model": "<model-id>", "api_version": "1"}
  • model is the server’s default model, used when a request omits model.
  • 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:

json
{"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 on param), not on message.
  • message: English, for people. It may change.
  • param: present when one field caused the error.
  • id: the request id, equal to the X-Request-Id header. Quote it when reporting a problem.
statustypewhen
400invalid_jsonthe 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, 505bad_requestthe HTTP request itself is malformed: bad request line (400), URL too long (414), too many or too large headers (431), unsupported HTTP version (505)
401unauthorizedthe request has no valid Authorization: Bearer <key>
404not_foundunknown path
404model_not_foundmodel is not one of the ids in GET /v1/models (param: model)
405method_not_allowedwrong 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)
413payload_too_largethe body is larger than the server’s limit (1,048,576 bytes by default). The message states the limit
422invalid_requestvalid JSON that breaks a rule of this reference: missing or unknown field, wrong type, a limit exceeded, repeated option or level names
422self_reference_not_supporteda decision asks about this service itself instead of about the context (param: the prompt, options or levels of that decision). Nothing was decided
422context_too_longthe 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
500internal_erroran unexpected failure. The message is generic; the request id lets whoever operates the service find it
503overloadedthe 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

parampoints to
modelthe model field
contextthe context field (missing, not an object, too large), and every context_too_long
decisionsthe decisions field, or an invalid decision name
decisions.team.kind, decisions.team.prompta field of one decision
decisions.team.options, decisions.mood.levelsthe whole list or object
decisions.team.options[1], decisions.mood.levels[0]one item of a list
decisions.team.options.billingthe description of one option
any other namean unknown top-level field

Headers

Send:

headerwhen
Content-Type: application/jsonon every POST
Content-Lengthon 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:

headervalue
Content-Typeapplication/json; charset=utf-8
X-Request-Idreq_ + 24 hexadecimal characters
Cache-Controlno-store
X-Content-Type-Optionsnosniff
Serveriryx
Access-Control-Allow-Originthe 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: Beareron 401
Allowon 405 for a known path
Retry-Afteron 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

whatlimit
request body1,048,576 bytes by default
context65,536 bytes, serialized as compact UTF-8 JSON
decisions per request1 to 32
decision name1 to 64 characters from A-Z a-z 0-9 _ . -
prompt1 to 2,000 characters
options (pick), levels (scale)2 to 26
option or level name1 to 200 characters
option description0 to 2,000 characters
silence on a connection60 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 oncesee 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 requestcontext, about
1 to 46,000 characters
83,000 characters
161,300 characters
32500 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 in GET /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, omit model.

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.