API documentation
AIvantage is an OpenAI- and Anthropic-compatible router. Your application changes only the base URL and the key; the calls otherwise stay the same.
Address and key
- OpenAI format
https://app.aivantage.fi/v1 - Anthropic format
https://app.aivantage.fi
OpenAI libraries use the address ending in /v1. Anthropic libraries and Claude Code add the path themselves, so give them the address without /v1.
You create the key for an application in the portal, and it is shown only once. Send it in the header Authorization: Bearer <AIVANTAGE_KEY> or x-api-key: <AIVANTAGE_KEY>. Either header works with either format. A wrong or revoked key returns 401 invalid_api_key.
API error messages are always in English.
Endpoints
| Endpoint | Purpose |
|---|---|
POST /v1/chat/completions | OpenAI Chat Completions. The cascade (aivantage/auto) or a locked model, streaming, tools and images. |
POST /v1/responses | OpenAI Responses API for provider-hosted tools (xAI x_search and web_search). xAI models only, no streaming. |
POST /v1/messages | Anthropic Messages. Anthropic models as they are, or aivantage/auto; the answer is always in Anthropic's format. |
POST /v1/messages/count_tokens | Anthropic's token counter, not billed. When Anthropic cannot count, for example when the cascade has no Anthropic model, the router estimates the count itself and adds the header x-aivantage-estimate: 1. |
GET /v1/models | The models the application may use. With the anthropic-version header, in Anthropic's format. |
GET /v1/models/{model_id} | One model in the same shape. An unknown model: 404 model_not_found. |
Models
| Model name | Behaviour |
|---|---|
aivantage/auto | The cascade. The request goes first to the policy's cheapest step, and quality gates move it to a stronger model only when needed. The steps are set in the portal. Works on /v1/chat/completions and /v1/messages. Without a model, /v1/chat/completions uses the cascade. |
aivantage/<id> | A locked model. The request goes straight to this model, without the cascade. The id also works without the prefix (e.g. claude-sonnet). GET /v1/models lists the models the application may use. |
gpt-5.5, grok-4.7 | Provider model names do not work in the OpenAI format: the answer is 404 model_not_found. Use an id from /v1/models. |
claude-sonnet-5-5 | On /v1/messages, Anthropic's own names, dated snapshots (e.g. -20251001) and the [1m] suffix also work. For other providers' models the answer is 400 messages_model_not_anthropic. |
The response's model field names the model that answered (aivantage/<id>). If the application is locked to one model in the portal, the request goes to it. If the lock rejects other models, a request for another model returns 409 app_model_locked.
Streaming, tools and images
Streaming (stream: true)
| Cascade | Works in both formats. The cascade settles the answer first, and the finished answer is sent as SSE chunks, so the first chunk arrives only when the answer is ready. If the policy's stream_mode is fixed, the stream starts at once from the fixed model. |
| Locked model | A true stream from the provider. On /v1/messages, Anthropic's events arrive unchanged. |
/v1/responses | No streaming: 400 stream_not_supported. |
Tools (function calling)
| Cascade | Works. A quality gate checks the tool call: the tool must be in the request and the arguments valid JSON with the required fields. An invalid call moves the request to the next step. |
| Locked model | Works if the model supports tools. Otherwise 403 capability_not_supported. |
/v1/messages | Anthropic's own tools (e.g. web search), documents and images in tool results work only with Anthropic models. In the cascade such a request skips the other steps. |
/v1/responses | The tools x_search, web_search and function. |
Images
| Cascade | Works (image_url; image in the Anthropic format). Steps whose target does not support images are skipped. |
| Locked model | Works if the model supports images. Otherwise 403 capability_not_supported. |
/v1/responses | input_image is passed to the provider. |
The privacy filter does not read images, so an image is always an uninspected attachment. If the policy blocks uninspected attachments, the request is refused before anything is sent: 403 privacy_blocked, reason attachment_not_inspected.
Headers
In the request
Authorization: Bearerx-api-key | The AIvantage key. Either one. |
X-AIvantage-End-User | An end-user id for the privacy log. Only a keyed hash of it is stored. Alternatively the OpenAI field safety_identifier or user, or the Anthropic field metadata.user_id. |
X-AIvantage-Session-Id | A session id for the privacy log. Claude Code's X-Claude-Code-Session-Id is read the same way. |
X-AIvantage-Privacy-Override | A reason for letting a privacy block through. Accepted only from a key the policy names, with an allowed reason and for identifier types the policy lets through. The request is always logged, accepted or not. |
anthropic-version | On /v1/models, the list and errors come back in Anthropic's format. On /v1/messages the header is not forwarded: the router sends its own version. |
anthropic-beta | Forwarded to Anthropic. Account-bound betas (e.g. files) are forwarded only on the tenant's own Anthropic key (BYOK). |
In the response
x-aivantage-request-id | The request id (req_…). The same id is in the portal's request log and in the request_id field of an error body. Give it to support. |
Retry-After | On a rate-limit 429: the seconds after which the request can be retried. Also x-ratelimit-limit-*, x-ratelimit-remaining-* and x-ratelimit-reset-*. |
x-aivantage-estimate | Value 1 when /v1/messages/count_tokens returns the router's own estimate. |
Errors
In the OpenAI format the reason is in error.code and the class in error.type: routing_error, policy_blocked, privacy_blocked, invalid_request_error or billing_error.
{"error": {"message": "The model is not allowed by the tenant's policy.",
"type": "policy_blocked", "code": "model_not_allowed",
"request_id": "req_…"}}
In the Anthropic format (/v1/messages) the type follows the HTTP status as in Anthropic's API, and the code is at the end of the message in square brackets. 503 no_healthy_instance is returned as 529 overloaded_error so that the SDKs retry. When the provider itself refuses the request, its own error is returned unchanged.
{"type": "error",
"error": {"type": "permission_error",
"message": "The model is not allowed by the tenant's policy. [model_not_allowed]"},
"request_id": "req_…"}
Retry on 429 (wait for Retry-After), 502 and 503. Other 4xx errors come from the request or the settings, and the same request will not succeed on retry.
| HTTP | error.code | Meaning |
|---|---|---|
| 400 | invalid_requestinvalid_request_error | The body is not JSON or a required field is missing (model, messages, input). |
| 400 | stream_not_supportedinvalid_request_error | /v1/responses does not stream. Send the request without stream: true. |
| 400 | unsupported_toolinvalid_request_error | /v1/responses accepts only the tools x_search, web_search and function. |
| 400 | responses_model_not_supportedrouting_error | /v1/responses only supports models with provider-hosted tools (xAI, e.g. aivantage/grok). |
| 400 | messages_model_not_anthropicrouting_error | This endpoint serves Anthropic models and aivantage/auto; use /v1/chat/completions for other providers' models. |
| 400 | messages_auto_needs_anthropicrouting_error | The request uses an Anthropic-only feature, but the cascade has no usable Anthropic model. |
| 400 | messages_shared_account_featurerouting_error | Files, containers, skills and MCP servers require the customer's own Anthropic key (BYOK); they are not available with the shared AIvantage key. |
| 400 | privacy_blockedprivacy_blocked | Blocked by the privacy filter: the request contained an identifier the customer's policy blocks. It was not sent to any model. |
| 401 | invalid_api_keyinvalid_request_error | The key is missing, wrong or revoked. In the Anthropic format the type is authentication_error. |
| 402 | pilot_endedbilling_error | The pilot has ended and the service has not been activated. Type billing_error, the same in the Anthropic format. |
| 402 | provider_billingrouting_error | The provider refused the key: credits exhausted or monthly limit reached. The message carries the provider's own explanation. |
| 403 | privacy_blockedprivacy_blocked | Blocked by the privacy filter: the request contained an identifier the customer's policy blocks. It was not sent to any model. |
| 403 | privacy_own_capacity_unsupportedprivacy_blocked | The request contained an identifier the customer's policy keeps on own capacity, and own capacity does not serve this endpoint; use /v1/chat/completions. The request was not sent to any model. |
| 403 | policy_invalidpolicy_blocked | The customer's processing policy is missing or invalid; the request was not processed. |
| 403 | model_not_allowedpolicy_blocked | The customer's policy doesn't allow this model. |
| 403 | no_eligible_instancepolicy_blocked | No target allowed by the policy serves this model. |
| 403 | capability_not_supportedpolicy_blocked | The model's provider does not support a feature the request uses (streaming, tools or images). |
| 403 | no_allowed_cascade_steppolicy_blocked | The cascade has no step allowed by the policy. |
| 403 | jurisdiction_not_allowedpolicy_blocked | The jurisdiction is not allowed by the policy. |
| 403 | external_not_allowedpolicy_blocked | External cloud services are not allowed by the policy. |
| 403 | provider_not_allowedpolicy_blocked | The provider is not allowed by the policy. |
| 403 | sovereignty_offpolicy_blocked | Sovereignty is switched off, so AIvantage's machines are not used. |
| 403 | dedicated_to_other_tenantpolicy_blocked | The instance is dedicated to another customer. |
| 403 | critical_requires_dedicatedpolicy_blocked | Only own capacity dedicated to the customer is allowed. |
| 403 | credential_not_configuredpolicy_blocked | The policy does not define whose key is used to call the provider (your own or the AIvantage key). |
| 403 | billing_agreement_requiredpolicy_blocked | AIvantage's own models require an approved billing agreement. Use your own API key (BYOK) or activate the service in the portal. |
| 403 | platform_key_not_allowedpolicy_blocked | AIvantage's API keys are not used for the customer's language model calls without an approved billing agreement. Use your own API key (BYOK). |
| 403 | app_model_not_allowedpolicy_blocked | The application's model is not in the allowed regions. Choose an allowed model for the application. |
| 403 | sovereignty_fi_unsupportedpolicy_blocked | The application's requests run only on AIvantage's hardware in Finland, and that hardware does not serve this endpoint; use /v1/chat/completions. The request was not sent to any model. |
| 404 | model_not_foundrouting_error | No such model. Use aivantage/<id> or a plain <id> from /v1/models. |
| 404 | endpoint_not_supportedinvalid_request_error | No such path (e.g. /v1/embeddings). The message lists the supported endpoints. |
| 405 | method_not_allowedinvalid_request_error | The path exists but the method is wrong (e.g. GET /v1/chat/completions). |
| 409 | app_model_lockedpolicy_blocked | The application is locked to one model, and its settings reject requests for other models. |
| 413 | context_length_exceededrouting_error | The input is longer than the context of the model the policy allows. No answer was returned. |
| 429 | usage_limit_tokenspolicy_blocked | The application's monthly token limit has been reached. |
| 429 | usage_limit_eurpolicy_blocked | The application's monthly spending limit (EUR) has been reached. |
| 429 | platform_spend_limitpolicy_blocked | The customer's monthly cap on AIvantage's provider keys has been reached. Use your own key (BYOK) or ask AIvantage to raise the cap. |
| 429 | rate_limit_exceededrequests | tokens | The tenant's rate limit is reached: requests or tokens per minute, or concurrent requests. The reason is in the reason field, the wait in the Retry-After header. |
| 4xx | upstream_rejectedrouting_error | The provider rejected the request. The HTTP status and the message are the provider's own. |
| 499 | client_disconnectedrouting_error | The client closed the stream. Shown only in the request log. |
| 500 | internal_errorrouting_error | An internal router error. Give the request_id to support. |
| 502 | all_targets_failedrouting_error | All targets allowed by the policy failed; the request was not routed elsewhere. |
| 502 | upstream_stream_errorrouting_error | The stream broke at the provider's end during the answer. The error arrives as the last SSE event. |
| 503 | no_healthy_instancerouting_error | All instances allowed by the policy are down. |
| 503 | locked_version_unavailablerouting_error | The locked model version is unavailable; the version is not changed without approval. |
| 503 | credential_missingrouting_error | The provider key is missing: your own key (BYOK) has not been saved or the AIvantage key has not been set. |
| 503 | app_model_unavailablepolicy_blocked | The application's locked model is not available right now, and no fallback model is chosen for the application. |
| 503 | sovereignty_fi_unavailablepolicy_blocked | The application's requests run only on AIvantage's hardware in Finland, and that hardware is not available right now. The request was not sent to any model. |
| 503 | privacy_filter_unavailableprivacy_blocked | The privacy filter could not finish its check, and the customer's policy requires one before sending (fail-closed). The request was not sent to any model. |
| 503 | privacy_own_capacity_unavailableprivacy_blocked | The request contained an identifier the customer's policy keeps on own capacity, and no own capacity the policy allows can serve it. The request was not sent to any model. |
Rate limits (429 rate_limit_exceeded)
The shape is the same as the providers' rate limits: error.type is requests or tokens, error.code is rate_limit_exceeded, and the router's own reason is in error.reason: tenant_rate_requests, tenant_rate_tokens, tenant_concurrency.
Privacy block (privacy_blocked)
A blocked request is not sent to any model. The error.privacy field gives the reason (reason), an explanation in Finnish and English (message_fi, message_en) and the locations (violations), never the blocked values.
| HTTP | error.privacy.reason | Meaning |
|---|---|---|
| 403 | entity_blocked | The request contains an identifier the policy blocks. Locations: error.privacy.violations (no values). |
| 403 | attachment_not_inspected | An attachment or image could not be inspected, and the policy blocks uninspected attachments. |
| 400 | unsupported_content | A message has a content block of a type the filter does not know. |
| 503 | request_too_long | There is more text to inspect than the tenant's settings allow. |
| 503 | filter_timeout | The check did not finish in time, and the policy requires it (fail-closed). |
| 503 | filter_error | The check failed, and the policy requires it (fail-closed). |
| 503 | privacy_own_capacity_unavailable | The request contained an identifier the customer's policy keeps on own capacity, and no own capacity the policy allows can serve it. The request was not sent to any model. |
| 403 | privacy_own_capacity_unsupported | The request contained an identifier the customer's policy keeps on own capacity, and own capacity does not serve this endpoint; use /v1/chat/completions. The request was not sent to any model. |
If an override was requested but not accepted, the error.privacy.override field gives the reason:
key_not_allowedThe policy does not let this key override the block.reason_not_allowedThe reason is not on the policy's list of allowed reasons.type_not_overridableThis identifier type may not be overridden.
Not supported
The router serves only the endpoints listed above. For example embeddings, image generation and audio are not available: /v1/embeddings, /v1/images/*, /v1/audio/*, /v1/files, /v1/batches, /v1/fine_tuning/*, /v1/moderations, /v1/completions, /v1/assistants, /v1/realtime.
An unknown /v1 path returns 404 and the supported endpoints:
{"error": {"message": "POST /v1/embeddings is not supported by AIvantage. Supported: …",
"type": "invalid_request_error", "param": null,
"code": "endpoint_not_supported"}}
Examples
Replace <AIVANTAGE_KEY> with the application's key. The application's page in the portal has the same examples with its models, plus the Claude Code and LangChain settings.
OpenAI · Python
from openai import OpenAI
client = OpenAI(
base_url="https://app.aivantage.fi/v1",
api_key="<AIVANTAGE_KEY>",
)
resp = client.chat.completions.create(
model="aivantage/auto",
messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)
OpenAI · JS/TS
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://app.aivantage.fi/v1",
apiKey: "<AIVANTAGE_KEY>",
});
const resp = await client.chat.completions.create({
model: "aivantage/auto",
messages: [{ role: "user", content: "Hello!" }],
});
console.log(resp.choices[0].message.content);
Anthropic · Python
import anthropic
client = anthropic.Anthropic(
base_url="https://app.aivantage.fi",
api_key="<AIVANTAGE_KEY>",
)
msg = client.messages.create(
model="aivantage/auto",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello!"}],
)
print(msg.content[0].text)
cURL
curl https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer <AIVANTAGE_KEY>" \
-H "Content-Type: application/json" \
-d '{"model": "aivantage/auto", "messages": [{"role": "user", "content": "Hello!"}]}'
curl https://app.aivantage.fi/v1/messages \
-H "x-api-key: <AIVANTAGE_KEY>" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model": "aivantage/auto", "max_tokens": 256, "messages": [{"role": "user", "content": "Hello!"}]}'
Test the features
These requests check that each feature works. First put the app's key in a variable: export AIVANTAGE_API_KEY=…
Shield's Mask, Own capacity only and Block, attachment blocking and Sovereignty are in use once the service is activated. During the pilot Shield only logs.
Key and model list
Checks that the key works and which models the app may use.
curl -s https://app.aivantage.fi/v1/models \
-H "Authorization: Bearer $AIVANTAGE_API_KEY"
What you should see: Status 200. The list has aivantage/auto, whose aivantage.cascade field holds the cascade steps, and every model the app may use. A model's aivantage.class field tells where it runs (own is AIvantage's own capacity, the others eu, us and cn), and aivantage.jurisdictions the countries.
Wrong key
Checks that a wrong key is refused.
curl -s https://app.aivantage.fi/v1/models \
-H "Authorization: Bearer vaara-avain"
What you should see: Status 401, error.code is invalid_api_key.
Cascade
Checks that the cascade picks a model and answers.
curl -si https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/auto","messages":[{"role":"user","content":"Vastaa yhdellä sanalla: mikä on Suomen pääkaupunki?"}]}'
What you should see: Status 200. The model field holds the model that answered (aivantage/<id>), one of the cascade models of aivantage/auto in the model list. An easy question usually goes to the first step. The x-aivantage-request-id header equals the id field, and the request shows in the portal's request log under that id. The usage field holds the billed tokens.
Locked model
Checks that the request goes to exactly the requested model. Use a model id from the model list.
curl -s https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/gpt-luna","messages":[{"role":"user","content":"Sano pelkkä: ok"}]}'
What you should see: Status 200, and the model field holds the requested model. A model not on the list returns 404 model_not_found.
Anthropic format
Checks that the Anthropic libraries' API works with the same key.
curl -s https://app.aivantage.fi/v1/messages \
-H "x-api-key: $AIVANTAGE_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/auto","max_tokens":50,"messages":[{"role":"user","content":"Sano pelkkä: ok"}]}'
What you should see: Status 200. The answer is in Anthropic's format (type is message), and the model field holds the model that answered (aivantage/<id>).
Streaming
Checks that the answer arrives as SSE chunks.
curl -sN https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/auto","stream":true,"messages":[{"role":"user","content":"Laske yhdestä viiteen."}]}'
What you should see: Lines data: {…} where object is chat.completion.chunk, with the same id and model in every chunk. Last comes a chunk with usage, then data: [DONE].
Shield: personal identity code
Checks what Shield does with a personal identity code. The model writes the code one character at a time, so the answer shows what the model received. Setting: The action on the Personal identity code row of the portal's Shield page.
curl -s https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/auto","messages":[{"role":"user","content":"Kirjoita tämä tunniste merkki kerrallaan välilyönnein erotettuna, ei muuta: 131052-308T"}]}'
| Setting | What you should see |
|---|---|
| Log | Status 200, and the answer has 1 3 1 0 5 2 - 3 0 8 T: the model received the value. The finding shows only in the portal. |
| Mask | Status 200, and the answer has H E T U _ 1: the model received a placeholder. When the model repeats the placeholder as is, AIvantage puts the original value back into the answer. |
| Own capacity only | Status 200, and the model field holds a model whose aivantage.class in the model list is own. |
| Block | Status 403. error.code is privacy_blocked and error.privacy.reason entity_blocked. The error.privacy.violations field names the location, never the value. |
Sovereignty
Checks that the request runs in Finland. Repeat the request without the identity code and compare. Setting: The app's Sovereignty. Critical in Finland needs the personal identity code on the Critical data list.
curl -s https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/auto","messages":[{"role":"user","content":"Henkilötunnus 131052-308T. Sano pelkkä: ok"}]}'
| Setting | What you should see |
|---|---|
| Critical in Finland | With the identity code, the model field holds a model whose aivantage.class in the model list is own and aivantage.jurisdictions FI. Without the code the request is routed as usual. |
| All in Finland | In both requests the model field holds a model whose aivantage.class is own and aivantage.jurisdictions FI. |
| Finnish capacity unavailable | Status 503 sovereignty_fi_unavailable when the setting is Block. With Allow to the cloud and log a cloud model answers. |
Uninspected attachment
Checks that an image Shield cannot read does not reach a model. Setting: Shield page, Advanced settings, Attachments: Block the request.
curl -s https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/auto","messages":[{"role":"user","content":[{"type":"text","text":"Mitä kuvassa on?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="}}]}]}'
What you should see: Status 403. error.code is privacy_blocked and error.privacy.reason attachment_not_inspected.
Rate limit
Checks that the limit stops a burst. The model aivantage/rajatesti does not exist, so the requests use no tokens.
seq 800 | xargs -P 50 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://app.aivantage.fi/v1/chat/completions \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/rajatesti","messages":[{"role":"user","content":"hei"}]}' | sort | uniq -c
What you should see: The output has a 429 line and the rest 404. A single 429 response has error.code rate_limit_exceeded, error.reason tenant_rate_requests and the headers Retry-After and x-ratelimit-limit-requests.
Unknown endpoint
Checks that an endpoint AIvantage does not serve says so.
curl -s https://app.aivantage.fi/v1/embeddings \
-H "Authorization: Bearer $AIVANTAGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"aivantage/auto","input":"hei"}'
What you should see: Status 404, error.code is endpoint_not_supported, and the message lists the supported endpoints.