The snowLEX API is OpenAI-compatible. Any OpenAI SDK or HTTP client works — just set the base URL and your snowLEX API key.
Two endpoints are exposed: POST /v1/chat/completions and GET /v1/models. Any other /v1/* path or verb returns a JSON 404 with code unknown_url (§09). What sets this API apart from a generic model endpoint: answers are grounded in indexed legal sources — Finnish national law via Finlex is the deepest coverage today, with EU material (EUR-Lex, CJEU) and Nordic sources indexed progressively — and the citations behind each answer are available machine-readably (§06). Coverage is expanding; when a question falls outside the index the model says so instead of inventing an answer, and you are not billed for a failed generation.
Pass your key as a bearer token. Create and manage keys in the console. Keys are shown once — store them securely. New keys carry the chat.completions scope by default; a key minted without it is rejected with 403 insufficient_scope.
Authorization: Bearer sk-snlx-...https://platform.snowlex.eu/v1POST/v1/chat/completions
The model is always snowlex-legal. Streaming (§05) and non-streaming are both supported. Non-streaming responses always include a usage object; when streaming, request the final usage chunk with stream_options.include_usage. The numbers in usage are the same numbers your balance is billed by (§13).
modelstring · requiredMust be snowlex-legal — anything else returns 404 model_not_found.
messagesarray · required1–50 messages, each with role and content. Content may be a string or an OpenAI content-part array (text parts are concatenated). Each message is limited to 32,000 characters (§11).
streamboolean · default falseStream the answer as OpenAI-format server-sent events (§05).
stream_optionsobject{"include_usage": true} emits a final usage chunk before data: [DONE] (§05).
snowlex_optionsobject · snowLEX extensionAnswer locale and grounded legal citations (§06).
max_tokens · temperature · top_p · stop · useraccepted, not appliedAccepted for SDK compatibility but not applied by the deterministic legal pipeline — see §07 before relying on them.
curl https://platform.snowlex.eu/v1/chat/completions \
-H "Authorization: Bearer sk-snlx-..." \
-H "Content-Type: application/json" \
-d '{
"model": "snowlex-legal",
"messages": [{"role": "user", "content": "Summarize GDPR Article 17."}],
"stream": false
}'{
"id": "chatcmpl-4d3c2b1a-9e8f-4a5b-b6c7-d8e9f0a1b2c3",
"object": "chat.completion",
"created": 1754640000,
"model": "snowlex-legal",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Article 17 GDPR establishes the right to erasure..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 5804,
"completion_tokens": 742,
"total_tokens": 6546
}
}Set "stream": true to receive OpenAI-format SSE chunks. Retrieval over the legal corpus runs before the first token, so the connection may be quiet for a while — during that phase the gateway emits SSE comment heartbeats (: ping) roughly every 15 seconds to keep proxies from dropping the connection. Standard SSE parsers and all OpenAI SDKs ignore comment lines; only handle them if you parse the byte stream yourself.
Chunks arrive in this order:
choices[0].delta.content).snowlex_sources chunk with choices: [] (§06).finish_reason: "stop").stream_options.include_usage: a final chunk with choices: [] and a usage object. It is always emitted — when exact usage is unavailable it carries the billed estimate flagged snowlex_estimated: true (§13).data: [DONE].If generation fails mid-stream you receive an OpenAI-style error event (the §09 envelope as a data: line) followed by data: [DONE] — and the request is not billed.
: ping ← SSE comment heartbeat (ignore)
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-legal","choices":[{"index":0,"delta":{"role":"assistant","content":"Article 17"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-legal","choices":[{"index":0,"delta":{"content":" GDPR establishes"},"finish_reason":null}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-legal","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-legal","choices":[],"usage":{"prompt_tokens":5804,"completion_tokens":742,"total_tokens":6546}}
data: [DONE]The headline feature of this API: grounded legal citations. Every answer is generated from retrieved passages of Finlex, EUR-Lex and CJEU material — set snowlex_options.include_sources and the API returns the citations behind the answer as structured data, ready to render as links or footnotes.
snowlex_options.locale"en" | "fi" | "sv" | "da" · default "en"Language of the answer. Unrecognized values fall back to en.
snowlex_options.include_sourcesboolean · default falseAttach the citations grounding the answer. Non-streaming: the response message carries message.snowlex_sources. Streaming: one extra chunk with choices: [] and a snowlex_sources array is emitted before the final stop chunk. Strictly opt-in, so clients that never ask never see a non-standard field.
idstringA stable id for the citation within this answer.
titlestringHuman-readable source title, e.g. a statute section or case number.
origin"finlex" | "eurlex" | "cjeu" | "other"Which corpus the source comes from.
excerptstringThe retrieved passage the answer relies on.
urlstring · optionalLink to the source document when available.
cited_in_paragraphsnumber[] · optional1-based indexes of the answer paragraphs that rely on this source.
curl https://platform.snowlex.eu/v1/chat/completions \
-H "Authorization: Bearer sk-snlx-..." \
-H "Content-Type: application/json" \
-d '{
"model": "snowlex-legal",
"messages": [{"role": "user",
"content": "Can an employer end employment during a trial period in Finland?"}],
"snowlex_options": {"locale": "en", "include_sources": true}
}'{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1754640000,
"model": "snowlex-legal",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "During a trial period both parties may cancel the employment contract with immediate effect, but not on discriminatory or otherwise inappropriate grounds...",
"snowlex_sources": [
{
"id": "src-1",
"title": "Employment Contracts Act (55/2001), Chapter 1, Section 4",
"origin": "finlex",
"excerpt": "During the trial period, the employment contract may be cancelled by either party...",
"url": "https://finlex.fi/en/legislation/...",
"cited_in_paragraphs": [1, 2]
},
{
"id": "src-2",
"title": "KKO:2009:35 (Supreme Court of Finland)",
"origin": "finlex",
"excerpt": "Cancellation during the trial period must not be based on grounds extraneous to its purpose...",
"cited_in_paragraphs": [3]
}
]
},
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 6112, "completion_tokens": 388, "total_tokens": 6500 }
}from openai import OpenAI
client = OpenAI(base_url="https://platform.snowlex.eu/v1", api_key="sk-snlx-...")
resp = client.chat.completions.create(
model="snowlex-legal",
messages=[{"role": "user",
"content": "Can an employer end employment during a trial period in Finland?"}],
extra_body={"snowlex_options": {"locale": "en", "include_sources": True}},
)
print(resp.choices[0].message.content)
for src in getattr(resp.choices[0].message, "snowlex_sources", []):
print(f"[{src['origin']}] {src['title']} {src.get('url', '')}")import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://platform.snowlex.eu/v1", apiKey: "sk-snlx-..." });
interface SnowlexSource {
id: string;
title: string;
origin: "finlex" | "eurlex" | "cjeu" | "other";
excerpt: string;
url?: string;
cited_in_paragraphs?: number[];
}
const resp = await client.chat.completions.create({
model: "snowlex-legal",
messages: [{ role: "user",
content: "Can an employer end employment during a trial period in Finland?" }],
// snowLEX extension — not in the OpenAI SDK's types
// @ts-expect-error vendor extension
snowlex_options: { locale: "en", include_sources: true },
});
const message = resp.choices[0].message as (typeof resp.choices)[0]["message"] & {
snowlex_sources?: SnowlexSource[];
};
console.log(message.content);
for (const src of message.snowlex_sources ?? []) {
console.log("[" + src.origin + "] " + src.title + (src.url ? " — " + src.url : ""));
}With stream: true, the sources arrive as one dedicated chunk right before the stop chunk:
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-legal","choices":[],"snowlex_sources":[{"id":"src-1","title":"Employment Contracts Act (55/2001), Chapter 1, Section 4","origin":"finlex","excerpt":"...","url":"https://finlex.fi/..."}]}
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-legal","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}The wire format is OpenAI's, but snowlex-legal is not a general-purpose sampled LLM — it is a deterministic legal-RAG pipeline. Parameters fall into three groups:
model, messages, stream, stream_options.include_usage, and the snowlex_options extension (§06).
max_tokens, temperature, top_p, stop and userare accepted so SDK defaults and existing call sites don't break, but the pipeline does not apply them: retrieval and generation are deterministic, and answers are bounded at roughly 4,096 output tokens regardless of max_tokens. Don't build behavior on these parameters.
tools, tool_choice, response_format, functions, logprobs and n > 1 return 400 unsupported_parameter (with param naming the field). These change the shape of the response, so they are rejected loudly rather than silently ignored.
Unknown /v1/* paths and wrong verbs return a JSON 404 with code unknown_url — never an HTML error page.
GET/v1/models
Returns the models available to your key. Requires authentication and is limited to 120 requests/minute per key.
curl https://platform.snowlex.eu/v1/models \
-H "Authorization: Bearer sk-snlx-..."{
"object": "list",
"data": [
{ "id": "snowlex-legal", "object": "model", "created": 1718200000, "owned_by": "snowlex" }
]
}All errors use the OpenAI error envelope — message, type, code and param (the offending field, or null):
{
"error": {
"message": "Monthly spend cap reached for this API key",
"type": "insufficient_quota",
"code": "spend_cap_exceeded",
"param": null
}
}503 service_unavailable is a retryable infrastructure failure — it never means your key or request is bad. SDKs should retry 503 and 429 with exponential backoff (honoring Retry-After on 429), and never retry 401. Chat-completion retries are double-charge-safe when you send an Idempotency-Key (§12).
Every response — success or error — carries an x-request-id header. Log it, and quote it in support requests: it is the id your charges are correlated by, so a disputed request can be traced end to end.
Each key carries its own requests-per-minute limit, chosen at creation in the console — default 60, self-serve maximum 300 (contact us for more). Exceeding it returns 429 rate_limit_exceeded with a Retry-After header. Successful responses and 429s both carry the current window state:
x-request-id: 4d3c2b1a-9e8f-4a5b-b6c7-d8e9f0a1b2c3
x-ratelimit-limit-requests: 60
x-ratelimit-remaining-requests: 57
x-ratelimit-reset-requests: 23sEach key can also carry an optional monthly spend cap, set in the console. Once reached, requests return 402 spend_cap_exceeded until the month rolls over or the cap is raised — a runaway integration can never spend more than the cap.
Request body≤ 1 MBLarger bodies return 413 body_too_large.
Messages per request≤ 50More return 400 invalid_body.
Characters per message≤ 32,000Longer messages return 400 invalid_body.
Output length~4,096 tokensAnswers are bounded by the pipeline; max_tokens is not applied (§07).
Send an Idempotency-Key header with a unique value per logical request. Retries carrying the same key reuse the same billing id, so a network-level retry can never double-charge you. Once a request with that key has completed, replaying it returns 409 idempotency_conflict instead of running (and billing) the request again. With an idempotency key the x-request-id is derived from it (an ik-… id), stable across retries.
curl https://platform.snowlex.eu/v1/chat/completions \
-H "Authorization: Bearer sk-snlx-..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: billing-4711-attempt" \
-d '{
"model": "snowlex-legal",
"messages": [{"role": "user", "content": "Summarize GDPR Article 17."}]
}'Billing is prepaid and token-metered: every internal step of the snowLEX pipeline is counted and summed per request, and the total is charged against your balance at the per-token prices on the pricing page. Each charge is itemized — request id, tokens in/out, cost, status — in the console's Usage view.
When the pipeline reports exact token usage, those numbers are exactly what your balance is charged. When it can't, the response's usage carries the reserved estimate, flagged with snowlex_estimated: true — and the billed amount matches those estimated numbers. Either way, usage always reflects the actual balance movement; there is no hidden charge beside it.
"usage": {
"prompt_tokens": 6100,
"completion_tokens": 800,
"total_tokens": 6900,
"snowlex_estimated": true
}Failed or empty generations are refunded: if you receive a 502 backend_error (or a mid-stream error event), the credit hold is released and nothing is billed.