Skip to main content
Ottili AI API

Ottili AI API — Quickstart

A copy-paste quickstart for the public, OpenAI-compatible Ottili AI API — authenticate, list models, send your first chat completion, stream tokens, and handle errors.

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/v1

This 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_xxx

Two 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):

StatuscodeMeaning
401invalid_api_keyMissing or invalid API key / JWT.
402insufficient_balanceManaged-credit balance (or worst-case reservation) too low.
403permission_errorMissing ai:chat scope, abused/suspended key, or tenant denial.
404model_not_foundUnknown, legacy (helix), or reserved (spire) model.
409idempotency_conflictReplay of an in-flight request with the same Idempotency-Key.
413prompt_too_largeEstimated prompt exceeded the model's context safety bound.
422invalid_request_errorRequest body failed schema validation.
429rate_limit_exceededRate limit (RPM/TPM) exceeded; honour Retry-After.
502upstream_errorProvider returned an error (raw message never leaked).
503upstream_timeoutProvider 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?