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 token
  • invalid api key
  • api key expired
  • account disabled
  • account 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/modelsList token-priced modelsFree
POST /v1/chat/completionsChat, tools, structured outputInput and output tokens
POST /v1/embeddingsVector embeddingsInput tokens
POST /v1/rerankRank documents against a queryInput tokens
POST /v1/systemoneSystem One: typed decisions with probabilitiesInput tokens
POST /v1/ocrDocument and image OCRPages
POST /v1/audio/transcriptionsSpeech to textAudio seconds
GET /v1/audio/transcriptions/realtime (WebSocket)Realtime speech to textAudio seconds
POST /v1/audio/speechText to speechCharacters
GET, POST /v1/audio/voicesList and create cloned voicesFree
GET, DELETE /v1/audio/voices/{id}Read or delete one cloned voiceFree
POST /v1/images/generationsImage generationImages
POST /v1/tools/web_search, web_news, web_fetchWeb toolsCalls, 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 for model in requests.
  • kind: chat, embeddings, or rerank.
  • context_length and max_output_length in tokens.
  • pricing: USD per token as decimal strings. pricing_eur has the EUR prices.
  • supported_sampling_parameters and supported_features (for example tools, json_mode, structured_outputs, reasoning).
  • reasoning_efforts: the reasoning_effort values this model accepts (for example none, low, high). Empty when stages are not configurable.
  • is_ready: false means no route is healthy right now. The model stays listed.
  • lifecycle: ga or preview. serving: hosted (the weights run on Nodion) or routed (forwarded to a provider).
  • supports_decisions: the model answers System One.
  • coding_plan: Codion may offer this chat model. Always true or false. 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 (or max_completion_tokens) the gateway sets max_tokens to the model's max_output_length. Larger values are lowered to it.
  • With "stream": true the answer is server-sent events (data: {...} lines, then data: [DONE]). The gateway sets stream_options.include_usage, so the last chunk normally carries usage. If the upstream connection breaks mid-stream, a last chunk carries an error object and finish_reason: "error", followed by data: [DONE].
  • The model field in the answer is the id you sent. Reasoning text, when a model returns it, is in reasoning_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 with supports_decisions.
  • state (required): what the questions are about. A string, an object, an array, or null.
  • 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 in answers.

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. criteria is optional and may only have the keys true and false. A noul needs non-empty instructions or at least one non-empty criterion.
  • choice: pick one label. criteria maps 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. criteria is 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 is true, from 0 to 1.
  • choice: choice is the most likely label, probabilities has every label and sums to 1, and confidence is a value from 0 to 1.
  • score: score is the probability-weighted mean of the level indexes (0 up to the number of levels minus 1). probabilities and legend are keyed by level index; legend maps each index to a level description. confidence is a value from 0 to 1.
  • model is the model that answered. usage.output_tokens is 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 detail text.
  • 404: model not found, also for models without supports_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 and firecrawl/pdf-inspector answer 400.
  • include_image_base64 (optional): return embedded images as base64. Not every OCR model returns images; some always answer with an empty images list.

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 uses request 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-After where 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_unavailable and reserve_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.

Give your agent a cloud.

Earn up to 15 € in credit when signing up.

Continue with GitHub

or with email

Stay in the loop.

Product news, no spam. Unsubscribe anytime.