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

§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

§04Chat completions

POST/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).

Request fields

modelstring · required

Must be 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 (§11).

streamboolean · default false

Stream 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 extension

Answer locale and grounded legal citations (§06).

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

Accepted for SDK compatibility but not applied by the deterministic legal pipeline — see §07 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-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
  }
}

§05Streaming

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: [] (§06).
  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 (§13).
  5. 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 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" | "other"

Which corpus the source comes from.

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

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-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', '')}")

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" | "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 : ""));
}

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-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"}]}

§07OpenAI compatibility

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:

Applied

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

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.

§08List models

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" }
  ]
}

§09Errors

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 model is always snowlex-legal
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 (§12).

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.

§10Rate 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.

§11Request 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 (§07).

§12Idempotency

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."}]
  }'

§13Usage & 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.