API reference.

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 (§10). What sets this API apart from a generic model endpoint: answers are grounded in indexed legal sources — one national corpus per jurisdiction (Finland, Sweden, Denmark, Germany, Norway, Iceland, Latvia, Lithuania, Estonia; EU material for Finland), chosen with the model field (§04) — and the citations behind each answer are available machine-readably (§07). 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.

§02Authentication

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-...

§03Base URL

https://platform.snowlex.eu/v1

§04Jurisdictions & models

snowLEX runs one legal model per country: its own corpus of statutes and case law, its own adapter. There is no country parameter — you pick the jurisdiction with the OpenAI model field. Choose by whose law applies, then ask in that country's language.

snowlex-fiFinland · fi · sv · en

Finlex, EUR-Lex, CJEU.

snowlex-seSweden · sv

Riksdagen (SFS), Swedish courts.

snowlex-dkDenmark · da

Retsinformation, Danish courts.

snowlex-deGermany · de

Gesetze im Internet, federal courts.

snowlex-noNorway · no

Lovdata, Norwegian courts.

snowlex-isIceland · is

Althingi statute book, reglugerd.is, Icelandic courts.

snowlex-lvLatvia · lv

likumi.lv.

snowlex-ltLithuania · lt

e-TAR, Lithuanian codes.

snowlex-eeEstonia · et

Riigi Teataja.

snowlex-legalalias · Finland

The original single model id. Still accepted, routes to snowlex-fi; responses echo whichever id you sent.

Answers follow the language of the question. The Finland model answers in Finnish, Swedish or English; the other national models are single-language and answer in their own language even when asked in English. Questions in an unsupported language are refused before retrieval, and a question the corpus does not cover (for example Swedish law sent to snowlex-fi) returns an answer that says so rather than a guess.

curl https://platform.snowlex.eu/v1/chat/completions \
  -H "Authorization: Bearer sk-snlx-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "snowlex-de",
    "messages": [{"role": "user",
      "content": "Welche Kündigungsfrist gilt nach zehn Jahren Betriebszugehörigkeit?"}],
    "snowlex_options": {"include_sources": true}
  }'

§05Chat completions

POST/v1/chat/completions

model picks the jurisdiction (§04). Streaming (§06) 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 (§14).

Request fields

modelstring · required

One of the jurisdiction models in §04 (snowlex-fi, snowlex-se, snowlex-dk, snowlex-de, snowlex-no, snowlex-is, snowlex-lv, snowlex-lt, snowlex-ee) or the alias snowlex-legal. Anything else returns 404 model_not_found.

messagesarray · required

1–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 (§12).

streamboolean · default false

Stream the answer as OpenAI-format server-sent events (§06).

stream_optionsobject

{"include_usage": true} emits a final usage chunk before data: [DONE] (§06).

snowlex_optionsobject · snowLEX extension

Fallback answer language and grounded legal citations (§07).

max_tokens · temperature · top_p · stop · useraccepted, not applied

Accepted for SDK compatibility but not applied by the deterministic legal pipeline — see §08 before relying on them.

Example

curl https://platform.snowlex.eu/v1/chat/completions \
  -H "Authorization: Bearer sk-snlx-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "snowlex-fi",
    "messages": [{"role": "user", "content": "Summarize GDPR Article 17."}],
    "stream": false
  }'
{
  "id": "chatcmpl-4d3c2b1a-9e8f-4a5b-b6c7-d8e9f0a1b2c3",
  "object": "chat.completion",
  "created": 1754640000,
  "model": "snowlex-fi",
  "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
  }
}

§06Streaming

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:

  1. Content deltas (choices[0].delta.content).
  2. If requested, one snowlex_sources chunk with choices: [] (§07).
  3. The stop chunk (finish_reason: "stop").
  4. With 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 (§14).
  5. data: [DONE].

If generation fails mid-stream you receive an OpenAI-style error event (the §10 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-fi","choices":[{"index":0,"delta":{"role":"assistant","content":"Article 17"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-fi","choices":[{"index":0,"delta":{"content":" GDPR establishes"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-fi","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-fi","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 the chosen jurisdiction's statutes and case law (§04) — 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" | "de" | "no"

Fallback answer language, used only when the question's language cannot be detected. Defaults to en on snowlex-fiand to the model's own language elsewhere; values a model cannot answer in are ignored (§04).

snowlex_options.include_sourcesboolean · default false

Attach 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.

The citation object

idstring

A stable id for the citation within this answer.

titlestring

Human-readable source title, e.g. a statute section or case number.

origin"finlex" | "eurlex" | "cjeu" | "riksdagen" | "retsinformation" | "gesetze" | "lovdata" | "other"

Which register the source comes from. Court decisions outside Finland and the EU report other; use jurisdiction to tell them apart.

jurisdiction"FI" | "SE" | "DK" | "DE" | "NO" | "EU"

The legal system the source belongs to.

excerptstring

The retrieved passage the answer relies on.

urlstring · optional

Link to the source document when available.

cited_in_paragraphsnumber[] · optional

1-based indexes of the answer paragraphs that rely on this source.

curl

curl https://platform.snowlex.eu/v1/chat/completions \
  -H "Authorization: Bearer sk-snlx-..." \
  -H "Content-Type: application/json" \
  -d '{
    "model": "snowlex-fi",
    "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-fi",
  "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",
            "jurisdiction": "FI",
            "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",
            "jurisdiction": "FI",
            "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 }
}

Python

from openai import OpenAI

client = OpenAI(base_url="https://platform.snowlex.eu/v1", api_key="sk-snlx-...")

resp = client.chat.completions.create(
    model="snowlex-fi",
    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', '')}")

TypeScript

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" | "riksdagen" | "retsinformation" | "gesetze" | "lovdata" | "other";
  jurisdiction: string;
  excerpt: string;
  url?: string;
  cited_in_paragraphs?: number[];
}

const resp = await client.chat.completions.create({
  model: "snowlex-fi",
  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 : ""));
}

Streaming

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-fi","choices":[],"snowlex_sources":[{"id":"src-1","title":"Employment Contracts Act (55/2001), Chapter 1, Section 4","origin":"finlex","jurisdiction":"FI","excerpt":"...","url":"https://finlex.fi/..."}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1754640000,"model":"snowlex-fi","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

§08OpenAI compatibility

The wire format is OpenAI's, but the snowLEX models are not general-purpose sampled LLMs — each is a deterministic legal-RAG pipeline. Parameters fall into three groups:

Applied

model, messages, stream, stream_options.include_usage, and the snowlex_options extension (§07).

Accepted but not applied

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.

Rejected

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.

§09List models

GET/v1/models

Returns the jurisdiction models this deployment serves (§04). 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-fi", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-se", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-dk", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-de", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-no", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-is", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-lv", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-lt", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-ee", "object": "model", "created": 1718200000, "owned_by": "snowlex" },
    { "id": "snowlex-legal", "object": "model", "created": 1718200000, "owned_by": "snowlex" }
  ]
}

§10Errors

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
  }
}
StatusCodeMeaning
400invalid_bodyMalformed JSON or an invalid field — param names the offending field
400unsupported_parametertools, tool_choice, response_format, functions, logprobs, or n > 1
401invalid_api_keyMissing or unknown API key — never retry automatically
402insufficient_creditsBalance can't cover the request — top up in the console
402spend_cap_exceededThis key's monthly spend cap is reached
403insufficient_scopeThe key lacks the chat.completions scope
404model_not_foundUnknown model id — the message lists the models this deployment serves (§04)
404unknown_urlUnknown /v1/* path or HTTP verb
409idempotency_conflictA request with this Idempotency-Key already completed
413body_too_largeRequest body over 1 MB
429rate_limit_exceededPer-key requests/minute exceeded — honor Retry-After
502backend_errorGeneration failed upstream — you are not billed
503service_unavailableTemporary infrastructure failure — retry with backoff

Retrying

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 (§13).

Request ids

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.

§11Rate limits & spend caps

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: 23s

Each 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.

§12Request limits

Request body≤ 1 MB

Larger bodies return 413 body_too_large.

Messages per request≤ 50

More return 400 invalid_body.

Characters per message≤ 32,000

Longer messages return 400 invalid_body.

Output length~4,096 tokens

Answers are bounded by the pipeline; max_tokens is not applied (§08).

§13Idempotency

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-fi",
    "messages": [{"role": "user", "content": "Summarize GDPR Article 17."}]
  }'

§14Usage & billing

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.

What the usage object means

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.