API reference
Authentication, the OpenAI-compatible endpoint surface, streaming, reasoning, and the error taxonomy.
Authentication
Every request to /v1/* authenticates with a virtual key in the Authorization: Bearer header. Mint keys in the Console; the secret is shown once and only its hash is stored. A key carries its own rate limits, budget, tags, an optional model allow-list (empty = any model), and sparse option_overrides that deviate from organisation policy (data-protection mode, NER model, injection-scan mode).
A request whose model is not on the key's non-empty allow-list is refused with a sealed 403 permission_error before anything is dispatched; the refusal itself lands in the audit chain.
Minting a production key requires a verified email address.
curl https://api.sluis.ai/v1/models \ -H "Authorization: Bearer $SLUIS_KEY" # a model outside the key's allow-list never dispatches: # → 403 permission_error · the refusal is sealed in the audit chain
{
"object": "list",
"data": [
{ "id": "mistral/mistral-large-latest", "object": "model", "owned_by": "mistral" },
{ "id": "vertex/claude-opus-4-8", "object": "model", "owned_by": "vertex" },
{ "id": "sluis/auto", "object": "model", "owned_by": "sluis" }
]
}Endpoints
Sluis exposes the OpenAI-compatible surface below. Endpoints without a first-class handler are proxied verbatim to the routed provider, streaming included, so OpenAI-compatible upstreams keep full fidelity.
| Endpoint | Purpose |
|---|---|
| POST /v1/chat/completions | Chat completions, the primary surface: routing, data protection, caching, streaming. |
| POST /v1/completions | Legacy text completions. |
| POST /v1/embeddings | Embeddings; also feeds the semantic cache. |
| POST /v1/moderations | Moderation classification. |
| POST /v1/responses | The OpenAI Responses API. |
| POST /v1/ocr | Document OCR (Mistral, or fully local with model sluis/ocr) · billed per page. With the dlp_documents policy on, inline documents are anonymized before they leave. |
| POST /v1/documents/anonymize | Document anonymization · PII swapped for «MERGE_TAG»s, images blurred · billed per page/image. |
| POST /v1/documents/anonymize/jobs | Async anonymization jobs · enqueue large documents, poll status, fetch the result via a time-limited signed URL that needs no API key. |
| GET /v1/models | The models your policy and credentials can actually reach, nothing hypothetical. |
| GET /v1/models/{id} | One model's metadata. |
| POST /v1/audio/* | Transcription, translation, speech · proxied to the routed provider. |
| POST /v1/images/* | Image generation and edits · proxied. |
| POST /v1/video/generations | Video generation · proxied. |
| /v1/files | File operations · proxied. |
| POST /v1/messages | Native Anthropic Messages ingress · point any Anthropic-SDK tool at Sluis; any connected model. |
| POST /v1/messages/count_tokens | Local token estimate for Anthropic-SDK context management; nothing is dispatched. |
| POST /v1beta/models/{model}:generateContent | Native Google Gemini ingress · point a Gemini SDK at Sluis (:streamGenerateContent streams). |
curl https://api.sluis.ai/v1/chat/completions \ -H "Authorization: Bearer $SLUIS_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mistral/mistral-large-latest", "messages": [{ "role": "user", "content": "Say hi" }] }'
{
"id": "chatcmpl-9f2e…",
"object": "chat.completion",
"model": "mistral-large-latest",
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "Hi! How can I help?" },
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 9, "completion_tokens": 8, "total_tokens": 17 }
}# Document OCR, billed per page. Returns pages[] markdown + usage_info.pages_processed. # model "sluis/ocr" runs the gateway's own OCR engine: fully local, no provider egress (inline data: URLs only). curl https://api.sluis.ai/v1/ocr \ -H "Authorization: Bearer $SLUIS_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "mistral/mistral-ocr-latest", "document": { "type": "document_url", "document_url": "https://example.com/invoice.pdf" } }'
{
"pages": [{
"index": 0,
"markdown": "# Invoice 2026-118\nAcme BV · Keizersgracht 1…",
"images": [],
"dimensions": { "dpi": 150, "height": 1754, "width": 1240 }
}],
"model": "mistral-ocr-latest",
"usage_info": { "pages_processed": 1, "doc_size_bytes": 48213 }
}# Document anonymization, billed per page/image. PII becomes «MERGE_TAG»s; images are blurred. curl https://api.sluis.ai/v1/documents/anonymize \ -H "Authorization: Bearer $SLUIS_KEY" \ -F file=@contract.docx \ -F 'options={ "entities": ["person_name", "email", "iban"], "include_mapping": true }'
{
"filename": "contract.docx",
"content_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"content_base64": "UEsDBBQABgAIA…",
"summary": { "pages": 4, "images": 1, "categories": ["EMAIL", "IBAN", "PERSON_NAME"], "downgraded": false },
"mapping": [
{ "token": "«PERSON_NAME_1»", "original": "Jan de Vries" },
{ "token": "«IBAN_1»", "original": "NL91ABNA0417164300" }
]
}Document input
Attach a PDF, Word, image, or plain-text file to a chat request as a "type": "file" content part carrying a base64 file_data data URL. Any routed model accepts it: providers that understand file parts natively receive them unchanged, and for every other provider the gateway converts the document before dispatch. Provider-side file_id references are not resolved; inline the bytes as file_data.
Vision-capable models get each PDF page rendered as an image_url part (48-page budget per request, shared across all attached documents); models without image input get the document's extractable text inlined as prompt text, which the data-protection scan then covers like any other text. Longer or heavier documents belong on /v1/ocr. With the dlp_documents policy on, the document is anonymized or refused before anything leaves the gateway. When it is off and the organisation's DLP mode is protective (tokenize, mask or block), documents that would travel as uninspectable images are refused; under allow_log they pass with an explicit unscanned marker in the audit trail.
# Attach a document as an OpenAI-style `file` content part. The gateway # normalizes it for the routed provider, so this works on any model. curl https://api.sluis.ai/v1/chat/completions \ -H "Authorization: Bearer $SLUIS_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "scaleway/gemma-4-26b-a4b-it", "messages": [{ "role": "user", "content": [ { "type": "file", "file": { "filename": "drawing.pdf", "file_data": "data:application/pdf;base64,'"$(base64 -w0 drawing.pdf)"'" } }, { "type": "text", "text": "Which discipline does this drawing document?" } ] }] }'
import OpenAI from "openai"; import { readFileSync } from "node:fs"; const client = new OpenAI({ baseURL: "https://api.sluis.ai/v1", apiKey: process.env.SLUIS_KEY, }); const pdf = readFileSync("drawing.pdf").toString("base64"); const reply = await client.chat.completions.create({ model: "vertex/gemini-3.1-pro", messages: [{ role: "user", content: [ { type: "file", file: { filename: "drawing.pdf", file_data: `data:application/pdf;base64,${pdf}` } }, { type: "text", text: "Which discipline does this drawing document?" }, ], }], });
Streaming
Set stream: true and the response arrives as server-sent events: each frame is a chat.completion.chunk delta and the stream ends with data: [DONE]. Streams are never buffered in the gateway: they tee through it, and the audit seal and metering happen even if the client hangs up early.
Ask for stream_options.include_usage and the final frame carries exact token usage, the same numbers the gateway meters and bills.
stream = client.chat.completions.create(
model="mistral/mistral-large-latest",
messages=[{"role": "user", "content": "Write a haiku"}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")const stream = await client.chat.completions.create({ model: "mistral/mistral-large-latest", messages: [{ role: "user", content: "Write a haiku" }], stream: true, stream_options: { include_usage: true }, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content ?? ""); }
# the raw event stream on the wire
data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Water"}}]}
data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" finds a way."}}]}
data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":9,"total_tokens":21}}
data: [DONE]Reasoning
Pass the OpenAI reasoning_effort parameter (minimal | low | medium | high) on any thinking-capable model. Sluis translates it per provider (Gemini's thinking level, Claude's adaptive thinking effort) and omits it where a model would reject it, so one parameter works across the whole catalog.
resp = client.chat.completions.create(
model="vertex/claude-opus-4-8",
messages=[{"role": "user", "content": "Prove it step by step…"}],
reasoning_effort="high", # minimal | low | medium | high
)Error codes
Errors use the OpenAI error envelope; the error.type value mirrors the HTTP status, so your SDK's error handling keeps working unchanged.
| Code | When |
|---|---|
| 400 invalid_request_error | Malformed request body or parameters. |
| 400 invalid_request_error | Model id missing its provider prefix. Every callable id is provider/model, e.g. mistral/mistral-large-latest; the body reads: model must be provider-prefixed. |
| 400 invalid_request_error | Request carries the removed x-sluis-dlp header. Per-request overrides were replaced by per-key option overrides; configure them in Console → API keys. |
| 401 authentication_error | Missing or unknown API key. |
| 402 insufficient_quota | No active plan or budget reached. Activate a plan or raise the budget; the request never reaches a provider. |
| 403 permission_error | The key lacks permission, for example the model is not on its allow-list. |
| 422 invalid_request_error | Refused before dispatch, for example data protection in block mode matched the request. |
| 422 document_too_large_to_scan | A document page stays over the render pixel cap even at the minimum scan resolution. Large-format pages (A0/A1 drawings) render downscaled automatically; the dedicated code lets a client shrink the page and resubmit. |
| 429 rate_limit_error | Rate limit reached. Enforced at the gateway; the request never hits a provider. |
| 451 permission_error | Blocked by residency policy: no allowed jurisdiction serves the request. The body includes the reason. |
| 5xx api_error | Upstream provider failure after retries; the circuit breaker steers traffic around unhealthy providers. |
{
"error": {
"message": "model must be provider-prefixed, e.g. mistral/mistral-large-latest",
"type": "invalid_request_error",
"param": null,
"code": "invalid_request"
}
}{
"error": {
"message": "model is not on this key's allow-list",
"type": "permission_error",
"param": null,
"code": "permission_denied"
}
}{
"error": {
"message": "blocked by residency policy: provider jurisdiction US is not in the allowed set [EU]",
"type": "permission_error",
"param": null,
"code": "permission_denied"
}
}