One line of code, and you run in the European Union.
The API is OpenAI-compatible. Existing SDKs, existing frameworks and existing code keep working: you swap the base_url and pick a tier instead of a model name.
Quickstart: your first API call
Create a key in your dashboard under Settings, API keys. You see the secret exactly once. After that we keep only a hash and the prefix. Put it in an environment variable, never in your repository.
curl https://api.europeanailab.eu/v1/chat/completions \
-H "Authorization: Bearer $UNION_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "eu-standard",
"messages": [{"role": "user", "content": "Explain VAT reverse charge."}]
}'import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["UNION_API_KEY"],
base_url="https://api.europeanailab.eu/v1", # the only line that changes
)
resp = client.chat.completions.create(
model="eu-standard",
messages=[{"role": "user", "content": "Explain VAT reverse charge."}],
)
print(resp.choices[0].message.content)Node, Go, Java, .NET?
https://api.europeanailab.eu/v1 and use your Union key. We implement the chat completions contract, including streaming, tools and JSON schemas.Model tiers and context windows
You pass a tier in the model field, not a model name. That keeps your integration working when we replace the model underneath, and we will, because open-weight models get retired and overtaken.
| Tier | Context | Input / 1M | Output / 1M | Best for |
|---|---|---|---|---|
eu-lite | 131K | €0.40 | €1.60 | Fast, cheap work: classification, extraction, short answers. |
eu-standard | 262K | €0.70 | €3.50 | The workhorse tier. Default for chat and most API traffic. |
eu-long | 1,000K | €1.00 | €2.00 | Whole case files, contracts and codebases in a single prompt. |
eu-pro | 1,000K | €4.00 | €12.00 | Heavy reasoning, agents, complex code. |
Exceed a tier's context window and you get a 400 with the maximum in it: we never silently truncate your prompt.
Streaming responses
Streaming works the way you expect, with one important difference you need to know about because it concerns your money.
Turn on include_usage
stream_options: {"include_usage": true} you cannot see at the end of a stream what the call cost. We meter it either way: this is about you being able to see and check it too.stream = client.chat.completions.create(
model="eu-standard",
messages=[{"role": "user", "content": "Write a summary."}],
stream=True,
# Without this there is no usage object and you cannot see the cost.
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
if chunk.usage: # final chunk
print("\n", chunk.usage)Cut a stream off halfway and you only pay for what was generated up to that point. If the broken connection means no usage chunk arrives, we estimate the consumption and correct it in the daily reconciliation with our provider. Such a line sits in your usage overview as estimated until it is settled.
Function calling and tool use
Tools follow the familiar schema. If a model produces a tool call that does not match your schema, we log it: in version 1 we do not silently correct it, because quietly repairing your output is worse than a visible failure.
tools = [{
"type": "function",
"function": {
"name": "find_invoice",
"description": "Look up an invoice by number.",
"parameters": {
"type": "object",
"properties": {"number": {"type": "string"}},
"required": ["number"],
},
},
}]
resp = client.chat.completions.create(
model="eu-standard", messages=messages, tools=tools
)Structured output with a JSON schema
Pass a JSON schema and validate the answer on your side. Our advice: always validate, even when the model usually behaves.
resp = client.chat.completions.create(
model="eu-standard",
messages=[{"role": "user", "content": "Extract the name and amount."}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "invoice",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"amount_eur": {"type": "number"},
},
"required": ["name", "amount_eur"],
},
},
},
)Response headers: cost, tier and routing
Every response tells you what happened. This is not a debugging extra, it is the point: you can check for yourself that you got what you paid for.
| Header | Meaning |
|---|---|
X-EU-Model-Tier | The tier this request ran on. |
X-EU-Routed | Present when the request was rerouted to another model, with the reason: censorship or error. |
X-EU-Degraded | true when the replacement model is weaker or has a smaller context window. You then pay the lower of the two rates. |
X-EU-Refusal-Class | Present on a refusal we could not reroute, for example A-unrouteable when no alternative has enough context. |
X-EU-Tokens-In / -Out | Metered tokens for this request. |
X-EU-Cost-EUR | What this request took off your balance, in euros. |
X-EU-Balance-EUR | Your remaining balance after this request. |
Rate limits and spend ceilings
There is a limit per key and per account, plus the ceilings you set yourself. The latter matter more than they sound: without a ceiling, a leaked key or an infinite loop in your own code can drain a balance overnight.
- Requests per minute, per key and per account.
- Daily and monthly ceiling in euros, configurable per key.
- New accounts: a maximum of €25 per day for the first 48 hours, whatever the balance.
- If we see more than ten times your weekly average within an hour, we pause the key automatically and email you. Better one false alarm than an empty wallet.
- A request only starts when your balance is at least €0.05.
Error codes and what to do
| Code | Meaning | What to do |
|---|---|---|
| 400 | Invalid request, or a prompt above the context window. | Fix the request; the maximum is in the error message. |
| 401 | Unknown, revoked or paused key. | Check the key in your dashboard. |
| 402 | Insufficient balance. | Top up. The response contains a direct link. |
| 403 | Request falls outside the usage policy. | See fair use below. |
| 429 | Rate limit or ceiling reached. | Wait and retry; respect Retry-After. |
| 502 / 503 | Upstream model unavailable and no fallback possible. | Retry. See the status page. |
Failed requests cost you nothing: if no usable answer came back, nothing comes off your balance.
Fair use and usage policy
We refuse instructions for weapons, malware and fraud, and material that harms children. Those refusals are final: we never forward such a request to another model to get an answer out of it anyway.
A model refusing a legitimate question about politics, history or a socially sensitive subject is a different matter: we treat that as a defect and send it to a model without that blind spot. You see it in the header X-EU-Routed: censorship.
One of our models is licensed with its own usage policy. That policy applies in full: rerouting does not get around licence terms.
Not sure whether your use case fits? Email hello@europeanailab.eu. An answer up front is cheaper for both of us than a blocked key afterwards.
Frequently asked questions
The questions that arrive by email before anyone opens a key: what the API is compatible with, what a request costs, and where it runs.
Which OpenAI SDKs work with an OpenAI-compatible API?
All the official ones, because they are thin clients over the chat completions contract and take the base URL as a constructor argument. The Python and Node SDKs are the common cases; Go, Java and .NET work the same way. You keep your retry logic, your streaming handling and your types.
Can I use LangChain or the Vercel AI SDK with it?
Yes. Both expose the base URL as configuration on their OpenAI provider, so you point that at our host and use a Union AI key. Anything else that lets you override the base URL works on the same principle, including most agent frameworks and evaluation harnesses.
Does the API support streaming and function calling?
Yes, both, plus structured output against a JSON schema. Streaming responses carry token usage at the end of the stream, so you can meter a streamed request as precisely as a normal one.
What are the rate limits?
There is a limit on requests per minute per key and per account, plus daily and monthly ceilings in euros that you set yourself. New accounts are capped at €25 a day for the first 48 hours, whatever the balance. Hitting a limit returns a 429 with a Retry-After header.
How should I handle 429s and retries?
Respect Retry-After rather than retrying immediately, and back off exponentially with jitter so a burst does not synchronise across your workers. Do not retry a 402: that means the balance is empty and a loop will simply repeat the failure. One retry is usually enough, because a second failure is rarely a different answer.
How do I see what a request cost?
Every response carries headers: X-EU-Tokens-In and X-EU-Tokens-Out for the metered tokens, X-EU-Cost-EUR for what came off your balance, and X-EU-Balance-EUR for what is left. Your own logs can therefore answer the cost question per feature or per customer without opening a dashboard.
Which model runs behind each tier?
Each tier maps to a named open-weight model, published with its licence and context window on the AI transparency page. You buy the tier rather than the model name, so we can replace the model underneath without breaking your integration, and we tell you when we do.
Moving an existing integration across is three lines: the key, the base URL and a tier name. The whole change, plus what does not port, is in switching in three lines. Still open? The rates per tier are on the pricing page, the model and licence behind each tier on AI transparency, and what happens during an outage on the status page. Two longer reads: what an AI API costs per token and where your prompts actually go.
Start with AI for Europeans.
Use AI without sending your data abroad, everything on European servers, at a European price.
€13 a month for the chat, cancel any time. Or per token from €10 of credit.
Not sure yet? Take the 60-second quiz →