This quickstart gets you from zero to your first API call in a few minutes. The Ottili AI API
is the public, versioned developer surface for Ottili AI. It is OpenAI-compatible*: the
request and response shapes match the OpenAI Chat Completions contract, so existing OpenAI
tooling can call it with a different base URL.
The canonical contract and full request/response/streaming/billing reference are part of the public API contract; the live API metadata endpoint is the authoritative source of truth. This page is the fastest path to a working call.
1. Base URL
All OpenAI-compatible endpoints are served from the public AI API host:
https://api.ai.ottili.one/v1This surface documents only the public developer contract. Internal Unified API routes, admin
endpoints, dashboard routes and local-development paths are intentionally not* part of this
surface.
2. Authenticate
Every request authenticates with the Authorization header:
Authorization: Bearer otk_live_xxxTwo credential types are accepted:
- Service API keys* — created in Ottili Auth, prefixed with
otk_, shown only once at
creation, stored hashed at rest.
- JWT bearer tokens* — issued by Ottili Auth for user sessions.
Your company and tenant context is resolved automatically* from the key's verified
membership. You do not* send tenant-id or company headers, and raw company-ID headers are
ignored. A missing or invalid key fails closed with 401.
3. Your first request
Pick a model id from GET /v1/models (see step 4). The public models are
ottili/vale-1.2 (efficient) and ottili/cairn-1.2 (premium); ottili/auto is the
adaptive router. ottili/vale-1.2 is used below as a concrete, working example.
curl -X POST "https://api.ai.ottili.one/v1/chat/completions" \
-H "Authorization: Bearer otk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "ottili/vale-1.2",
"messages": [
{ "role": "system", "content": "You are a concise assistant." },
{ "role": "user", "content": "Summarise the open orders of a customer in one sentence." }
]
}'Response (OpenAI-compatible):
{
"id": "chatcmpl_ottili_9f2c1a4b6e",
"object": "chat.completion",
"created": 1783987200,
"model": "ottili/vale-1.2",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "The customer has 3 open orders totalling €1,240, all awaiting fulfilment." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 18, "completion_tokens": 14, "total_tokens": 32 }
}The usage field is always present on the OpenAI-compatible surface; when the upstream
provider did not report exact counts, usage.source is "estimated" (you can label
estimated usage honestly — it is never presented as exact).
4. List available models
curl "https://api.ai.ottili.one/v1/models" \
-H "Authorization: Bearer otk_live_xxx"Response (OpenAI-compatible list shape; capability flags and pricing are Ottili extensions
ignored by strict OpenAI clients):
{
"object": "list",
"data": [
{
"id": "ottili/vale-1.2",
"object": "model",
"owned_by": "ottili",
"context_length": 1000000,
"pricing": { "prompt": "0.000000200", "completion": "0.000000400" },
"supports_streaming": true,
"supports_tools": true,
"supports_json_mode": true,
"tier": "fast",
"lifecycle_status": "active",
"capabilities": { "streaming": true, "tools": true, "json_schema": true, "reasoning": false }
}
]
}Pass the id value (e.g. ottili/vale-1.2) as the model field in your chat requests.
5. Stream tokens
Set "stream": true to receive tokens as they are generated (text/event-stream):
curl -N -X POST "https://api.ai.ottili.one/v1/chat/completions" \
-H "Authorization: Bearer otk_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "ottili/vale-1.2",
"stream": true,
"messages": [
{ "role": "user", "content": "Draft a short follow-up email to a pending quote." }
]
}'Each event is a data: line carrying a JSON delta, ending with data: [DONE]:
data: {"id":"chatcmpl_ottili_7a1b","object":"chat.completion.chunk","created":1783987300,"model":"ottili/vale-1.2","choices":[{"index":0,"delta":{"role":"assistant"}}]}
data: {"id":"chatcmpl_ottili_7a1b","object":"chat.completion.chunk","created":1783987300,"model":"ottili/vale-1.2","choices":[{"index":0,"delta":{"content":"Hi"}}]}
data: {"id":"chatcmpl_ottili_7a1b","object":"chat.completion.chunk","created":1783987300,"model":"ottili/vale-1.2","choices":[{"index":0,"delta":{"content":" there"},"finish_reason":"stop","usage":{"prompt_tokens":12,"completion_tokens":8,"total_tokens":20}}]}
data: [DONE]Consume the stream in JavaScript:
const res = await fetch("https://api.ai.ottili.one/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OTTILI_AI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "ottili/vale-1.2",
stream: true,
messages: [{ role: "user", content: "Draft a short follow-up email." }],
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
for (const line of decoder.decode(value).split("\n")) {
if (!line.startsWith("data:") || line.includes("[DONE]")) continue;
const chunk = JSON.parse(line.slice(5));
if (chunk.choices?.[0]?.delta?.content) process.stdout.write(chunk.choices[0].delta.content);
}
}6. Handle errors
Errors use standard HTTP status codes with a machine-readable envelope that carries a stable
code and a human-readable message:
{
"error": {
"message": "Model `ottili/helix-1.2` is not available.",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found",
"ottili": { "request_id": "req_...", "trace_id": "trc_...", "retryable": false, "retry_after": null }
}
}Common cases (see the
[Errors](/docs/ottili-ai-api-errors) reference for the full table):
| Status | code | Meaning |
|---|---|---|
401 | invalid_api_key | Missing or invalid API key / JWT. |
402 | insufficient_balance | Managed-credit balance (or worst-case reservation) too low. |
403 | permission_error | Missing ai:chat scope, abused/suspended key, or tenant denial. |
404 | model_not_found | Unknown, legacy (helix), or reserved (spire) model. |
409 | idempotency_conflict | Replay of an in-flight request with the same Idempotency-Key. |
413 | prompt_too_large | Estimated prompt exceeded the model's context safety bound. |
422 | invalid_request_error | Request body failed schema validation. |
429 | rate_limit_exceeded | Rate limit (RPM/TPM) exceeded; honour Retry-After. |
502 | upstream_error | Provider returned an error (raw message never leaked). |
503 | upstream_timeout | Provider timed out. |
A robust client reads the code to decide retry vs. surface-to-user, and treats any
non-2xx status as an error.
const res = await fetch("https://api.ai.ottili.one/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OTTILI_AI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ model: "ottili/vale-1.2", messages: [{ role: "user", content: "Hello" }] }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
const code = err?.error?.code ?? `HTTP_${res.status}`;
console.error(`Request failed (${code}):`, err?.error?.message ?? res.statusText);
process.exit(1);
}
const data = await res.json();
console.log(data.choices[0].message.content);7. Minimal Python client
import os, requests
API_KEY = os.environ["OTTILI_AI_API_KEY"]
BASE = "https://api.ai.ottili.one/v1"
resp = requests.post(
f"{BASE}/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json={
"model": "ottili/vale-1.2",
"messages": [{"role": "user", "content": "What is my company's credit balance?"}],
},
timeout=30,
)
resp.raise_for_status()
print(resp.json()["choices"][0]["message"]["content"])8. Rate limits and credits
AI usage is metered against your company credit wallet. Each billable request takes a
worst-case reservation before work begins and settles on success; a replay with the same
Idempotency-Key never double-charges. See
[Credits & usage](/docs/ottili-ai-api-credits) and
[Rate limits](/docs/ottili-ai-api-rate-limits) for details, and
[GET /v1/models](/docs/ottili-ai-api-models) for the live model list.
9. Next steps
- [Ottili AI API overview](/docs/ottili-ai-api) — full contract reference.
- [Models & availability](/docs/ottili-ai-api-models)
- [Streaming](/docs/ottili-ai-api-streaming)
- [Errors](/docs/ottili-ai-api-errors)
- [Tool calls](/docs/ottili-ai-api-tool-calls) and [Structured outputs](/docs/ottili-ai-api-structured-outputs)
- [Ottili AI API overview](/docs/ottili-ai-api) — the full public API contract, billing, migration & rollback.
Was this article helpful?
