AI API reference
Every endpoint on api.nodion.ai/v1, with limits and errors.
Base URL: https://api.nodion.ai/v1. The API follows the OpenAI shape, so OpenAI SDKs work
when you point them at this base URL. For keys, the wallet, and the dashboard tabs, see
AI Inference.
pythonimport os
from openai import OpenAI
client = OpenAI(
base_url="https://api.nodion.ai/v1",
api_key=os.environ["NODION_AI_KEY"],
)
reply = client.chat.completions.create(
model="<model>",
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content) Authentication
Send an inference key (nodion_ai_…) as a bearer token on every request. Platform keys
(nodion_…) do not work here. GET /v1/models is the only call that needs no key.
httpAuthorization: Bearer nodion_ai_… A rejected key answers 401 with one of these messages:
missing bearer tokeninvalid api keyapi key expiredaccount disabledaccount under review
Every response carries an X-Request-ID header. Quote it when you contact support.
JSON request bodies can be up to 1 MiB (System One 512 KiB). Base64 adds a third, so a file sent
inline in JSON, such as an image, a PDF, or an OCR document, can be about 750 KB. Multipart uploads to
/v1/audio/transcriptions and /v1/audio/voices can be up to 25 MiB, with
transcription files up to 20 MiB and voice sample files up to 10 MiB (11 MiB for the whole
form).
Endpoints
| Endpoint | What it does | Billed by |
|---|---|---|
GET /v1/models | List token-priced models | Free |
POST /v1/chat/completions | Chat, tools, structured output | Input and output tokens |
POST /v1/embeddings | Vector embeddings | Input tokens |
POST /v1/rerank | Rank documents against a query | Input tokens |
POST /v1/systemone | System One: typed decisions with probabilities | Input tokens |
POST /v1/ocr | Document and image OCR | Pages |
POST /v1/audio/transcriptions | Speech to text | Audio seconds |
GET /v1/audio/transcriptions/realtime (WebSocket) | Realtime speech to text | Audio seconds |
POST /v1/audio/speech | Text to speech | Characters |
GET, POST /v1/audio/voices | List and create cloned voices | Free |
GET, DELETE /v1/audio/voices/{id} | Read or delete one cloned voice | Free |
POST /v1/images/generations | Image generation | Images |
POST /v1/tools/web_search, web_news, web_fetch | Web tools | Calls, plus each page a web_search fetches |
Audio, images, and tools are described with examples on AI Inference. The rest is below.
Before a call the wallet holds an upper bound of its cost. The request then settles on the reported usage,
never above the hold, and the rest is released. If a successful answer reports no usage, the full hold is
charged. A realtime transcription session holds the model's max_audio_seconds (1,800 seconds
if the model sets none), so the wallet has to cover a full session up front.
Models
GET /v1/models returns {"object": "list", "data": [...]} and lists chat,
embeddings, and rerank models. OCR, speech, image, and tool models are not in it; the public
catalog at https://api.nodion.com/v2/public/ai/models lists every kind. Useful fields per model:
id: the value formodelin requests.kind:chat,embeddings, orrerank.context_lengthandmax_output_lengthin tokens.pricing: USD per token as decimal strings.pricing_eurhas the EUR prices.supported_sampling_parametersandsupported_features(for exampletools,json_mode,structured_outputs,reasoning).reasoning_efforts: thereasoning_effortvalues this model accepts (for examplenone,low,high). Empty when stages are not configurable.is_ready:falsemeans no route is healthy right now. The model stays listed.lifecycle:gaorpreview.serving:hosted(the weights run on Nodion) orrouted(forwarded to a provider).supports_decisions: the model answers System One.coding_plan: Codion may offer this chat model. Alwaystrueorfalse. This is not the billing coding plan.free_until: set while a free window is active on a model served only by Nodion. The token prices then read 0.
Chat completions
POST /v1/chat/completions takes the OpenAI Chat Completions body: model,
messages, and the optional fields the model lists in supported_sampling_parameters
and supported_features, such as tools or response_format.
Send reasoning_effort only when reasoning_efforts is non-empty, and only a value from that list.
bashcurl https://api.nodion.ai/v1/chat/completions \
-H "Authorization: Bearer $NODION_AI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<model>",
"stream": true,
"max_tokens": 512,
"messages": [
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "What is an embedding?"}
]
}' What the gateway does to the request and the answer:
- Without
max_tokens(ormax_completion_tokens) the gateway setsmax_tokensto the model'smax_output_length. Larger values are lowered to it. - With
"stream": truethe answer is server-sent events (data: {...}lines, thendata: [DONE]). The gateway setsstream_options.include_usage, so the last chunk normally carries usage. If the upstream connection breaks mid-stream, a last chunk carries anerrorobject andfinish_reason: "error", followed bydata: [DONE]. - The
modelfield in the answer is the id you sent. Reasoning text, when a model returns it, is inreasoning_content.
PDFs can go inline as a file content block with a base64 data URI. The gateway renders the first
20 pages to images before the call, so this only helps on models with image input. file_id
references are rejected.
json{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this contract."},
{"type": "file", "file": {"file_data": "data:application/pdf;base64,JVBERi0..."}}
]
}
Billing: input and output tokens at the model's price, cached input at the cache price where the route
reports it. The hold counts every request byte as an input token, plus max_tokens. For requests
with images or PDFs the input part is raised to the model's context length, or to at least 128,000 tokens
when the request is larger than the context. Inside a model's free window
(free_until), chat completions on hosted routes are free.
Embeddings
POST /v1/embeddings takes the OpenAI body (model, input as a string or
an array) and returns the OpenAI embeddings response. No streaming. Billed on usage.prompt_tokens.
Use a model with kind: "embeddings".
bashcurl https://api.nodion.ai/v1/embeddings \
-H "Authorization: Bearer $NODION_AI_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "<embeddings-model>", "input": ["first text", "second text"]}' Rerank
POST /v1/rerank scores documents against a query. Optional
top_n limits the results. The answer has results with index (position
in documents) and relevance_score, plus usage.prompt_tokens, which is
what you are billed on. Use a model with kind: "rerank".
bashcurl https://api.nodion.ai/v1/rerank \
-H "Authorization: Bearer $NODION_AI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "<rerank-model>",
"query": "How do I reset my password?",
"documents": ["Billing FAQ", "Account recovery guide", "Release notes"],
"top_n": 2
}' System One
POST /v1/systemone answers typed questions about a piece of state. The model scores every
possible answer instead of writing text, so you get probabilities back, not prose. Use it for routing,
triage, moderation, or as a gate before an agent runs a tool. One call answers up to 16 questions about the
same state.
Models with supports_decisions: true on /v1/models serve it. System One only runs on
Nodion-hosted routes, never on routed providers. The Decisions tab in the Playground has
ready-made examples.
bashcurl https://api.nodion.ai/v1/systemone \
-H "Authorization: Bearer $NODION_AI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "aleph-alpha/kolibri-1",
"state": {
"customer_tier": "enterprise",
"ticket": "My checkout page shows a blank screen after I click Pay."
},
"questions": {
"is_bug": {
"type": "noul",
"instructions": "Is the customer reporting a software defect?"
},
"team": {
"type": "choice",
"instructions": "Which team should own this ticket?",
"criteria": {
"payments": "Checkout, billing, or payment processing issues.",
"frontend": "Rendering, layout, or browser issues.",
"account": "Login, permissions, or profile issues."
}
},
"urgency": {
"type": "score",
"instructions": "How urgent is this ticket?",
"criteria": [
"Can wait for the next release",
"Should be fixed this week",
"Blocking revenue right now"
]
}
}
}' Request
model(required): a model withsupports_decisions.state(required): what the questions are about. A string, an object, an array, ornull.questions(required): an object with 1 to 16 entries. The key is your name for the question (up to 128 characters) and comes back as the key inanswers.
Each question has a type, optional instructions, and criteria that
depend on the type. Instructions and criteria values can be a string, an object, or an array;
null is allowed everywhere except for score levels.
noul: a yes/no question.criteriais optional and may only have the keystrueandfalse. Anoulneeds non-emptyinstructionsor at least one non-empty criterion.choice: pick one label.criteriamaps 1 to 26 labels to a value; blank strings are rejected. Labels must be unique, ignoring case, and up to 256 characters.score: rate on a scale.criteriais an array of 2 to 10 non-empty levels, lowest first. Level indexes start at 0.
Unknown fields are rejected. The body can be up to 512 KiB, and the state plus the largest question must fit the model's context.
Response
json{
"model": "aleph-alpha/kolibri-1",
"answers": {
"is_bug": {"type": "noul", "noul": 0.91},
"team": {
"type": "choice",
"choice": "payments",
"confidence": 0.78,
"probabilities": {"payments": 0.82, "frontend": 0.15, "account": 0.03}
},
"urgency": {
"type": "score",
"score": 1.5,
"confidence": 0.6,
"legend": {
"0": "Can wait for the next release",
"1": "Should be fixed this week",
"2": "Blocking revenue right now"
},
"probabilities": {"0": 0.1, "1": 0.3, "2": 0.6}
}
},
"usage": {"input_tokens": 412, "output_tokens": 0}
} noul: the probability that the answer istrue, from 0 to 1.choice:choiceis the most likely label,probabilitieshas every label and sums to 1, andconfidenceis a value from 0 to 1.score:scoreis the probability-weighted mean of the level indexes (0 up to the number of levels minus 1).probabilitiesandlegendare keyed by level index;legendmaps each index to a level description.confidenceis a value from 0 to 1.modelis the model that answered.usage.output_tokensis always 0.
The values in the example response are illustrative.
Billing
Only input tokens are billed, at the model's input price. The model reads the state once per question, so
usage.input_tokens grows with the number of questions. Before the call the wallet holds
(bytes of the state + bytes of all questions + 4,096) × the number of questions as input tokens,
and the request settles on usage.input_tokens, never above the hold. Requests that fail are not
billed; if you disconnect after the model has answered, its input tokens are billed. Free windows apply as
for chat.
Errors
System One uses its own error body: {"detail": "message"}, and for 422 a list of
loc / msg / type items that point at the field.
json{
"detail": [
{"loc": ["body", "questions", "team", "criteria"], "msg": "choice accepts 1 to 26 criteria", "type": "too_long"}
]
} - 401: missing or invalid key.
- 402: billing refused the request: balance, spend limits, or the request could not be priced. The reasons are those of the 402 codes, here only as
detailtext. - 404:
model not found, also for models withoutsupports_decisions. - 422: invalid body, body over 512 KiB, state and questions over the context, or the model rejected the request.
- 429: a key limit was hit.
Retry-After: 5. - 502: the model failed to answer.
- 503: no healthy route, none that matches the project's privacy settings, or the settings could not be loaded.
- 529: every route for the model is at capacity.
Retry-After: 5.
OCR
POST /v1/ocr follows the Mistral OCR shape. Send the file inline as a base64 data URI;
remote URLs are rejected. PDF, PNG, JPEG, and WebP work.
bashcurl https://api.nodion.ai/v1/ocr \
-H "Authorization: Bearer $NODION_AI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "mistral/mistral-ocr-4-1",
"document": {
"type": "document_url",
"document_url": "data:application/pdf;base64,JVBERi0..."
}
}' model: an OCR model from the public catalog (kind: "ocr").document:{"type": "document_url", "document_url": "data:..."}or{"type": "image_url", "image_url": "data:..."}.pages(optional): page indexes to read, starting at 0. Not every OCR model supports it; hosted OCR models andfirecrawl/pdf-inspectoranswer 400.include_image_base64(optional): return embedded images as base64. Not every OCR model returns images; some always answer with an emptyimageslist.
The answer has pages (each with index, markdown, and
images) and usage_info.pages_processed. Billed per processed page. The hold covers
the whole document, and documents over the model's max_pages are rejected.
firecrawl/pdf-inspector reads digitally generated PDFs only. An image answers 422; a PDF with
scanned pages answers 422 with code: "pages_need_ocr" and the affected pages in
pages_needing_ocr.
Errors and limits
Every endpoint except System One answers errors in the OpenAI envelope:
json{
"error": {
"message": "insufficient AI balance. Top up in the AI Billing tab.",
"type": "error",
"code": "empty_balance"
}
} message is always there. type, and whether code is set, vary by
endpoint, so branch on the HTTP status first. Chat, embeddings, rerank, and the web tools put the
402 codes in code. When the model's route answers chat, embeddings, or
rerank with an HTTP error, the body also carries request_id. The X-Request-ID
header is always there.
- 400: invalid JSON, missing
model, or a model of the wrong kind for the endpoint (for example an OCR model on chat). A body over 25 MiB that reaches the API process usesrequest body exceeds 25 MiB. - 401: see Authentication.
- 402: the wallet refused the request; see the codes below.
- 404:
model not found, or an endpoint that does not exist. - 413: the public host refused a body larger than 25 MiB before the API ran.
- 429: a key limit was hit, or the model is overloaded. Respect
Retry-Afterwhere it is sent. - 502: the model's route failed. Retry later.
- 503: no healthy route for the model, none that matches the project's privacy settings (Settings tab), or the settings could not be loaded.
402 codes. Chat, embeddings, rerank, and the web tools send them in code. OCR, audio, and image
send only the message, and System One sends it as detail.
empty_balance: the available AI balance does not cover the hold for this request. Top up in the Billing tab, or turn on auto top-up.daily_quota: today's spend on this key plus its open holds plus this request would pass the key's daily spend limit.month_quota: this month's spend plus open holds plus this request would pass the monthly spend limit you set in the Billing tab.usage_limit: this month's spend plus open holds plus this request would pass the monthly cap of your usage tier. The Billing tab shows the tier and when the next one applies.pricing_unavailableandreserve_failed: the request could not be priced or held. Retry, then contact support with the request id.
Default limits per key:
- 300 requests per minute
- 2,000,000 tokens per minute (chat, embeddings, rerank, and System One)
- 10 concurrent requests
- 25 MiB request body on chat, embeddings, rerank, OCR, audio, and images. Tools are 1 MiB. System One is 512 KiB.
On chat, embeddings, and rerank a 429 sends Retry-After: 5 for the request rate and for
overload, 10 for the token rate, and 2 for concurrency. System One always sends
Retry-After: 5. Creating a voice sends Retry-After when its own limits hit (too
many uploads in progress, or more than ten create attempts per hour). Other 429s on OCR, audio, image, the web tools, and
voices send none; wait a few seconds before you retry. Contact support if you need higher limits.