# InnoAI API Guide

InnoAI is a private AI inference API. It is compatible with the OpenAI API, so existing OpenAI SDKs and tools work by changing the base URL and the API key.

**Base URL:** `https://api.innoai.net/v1`

All requests and responses are JSON over HTTPS, except audio uploads (multipart form data) and generated audio (binary).

## Quick start

```bash
curl https://api.innoai.net/v1/chat/completions \
  -H "Authorization: Bearer $INNOAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "fast",
    "messages": [{"role": "user", "content": "Say hello in five words."}]
  }'
```

## Authentication

Every API request needs an API key in the `Authorization` header:

```
Authorization: Bearer ina_...
```

- Keys start with `ina_` and are 76 characters long.
- **Getting a key:** sign in at [https://api.innoai.net](https://api.innoai.net), open **My dashboard**, and use **Create a key**. You can give a key a name and an optional expiry in days.
- **Shown once:** the full key is displayed only at creation. InnoAI stores only a hash and cannot show it again. If you lose a key, revoke it and create a new one.
- **Revocation:** revoke a key from the dashboard at any time. Revoked, disabled and expired keys are rejected immediately with `401`.
- A key can be narrowed by your administrator to specific models or lower limits. A model the key may not use returns `404` as if it did not exist.
- Keep keys secret: never put them in client-side code or public repositories.

## OpenAI compatibility

Use the official OpenAI SDKs with the InnoAI base URL.

**Python**

```python
from openai import OpenAI

client = OpenAI(base_url="https://api.innoai.net/v1", api_key="ina_...")

resp = client.chat.completions.create(
    model="fast",
    messages=[{"role": "user", "content": "Summarise the water cycle in one sentence."}],
)
print(resp.choices[0].message.content)
```

**Node.js**

```javascript
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.innoai.net/v1", apiKey: process.env.INNOAI_API_KEY });

const resp = await client.chat.completions.create({
  model: "fast",
  messages: [{ role: "user", content: "Summarise the water cycle in one sentence." }],
});
console.log(resp.choices[0].message.content);
```

Responses follow the OpenAI formats. Some responses carry additional fields (for example `stop_reason` or `token_ids`); ignore fields you do not use. The `model` field in a response is always the model name you requested. `system_fingerprint` is always `null`.

## Models

| Model | Type | Best for | Thinking | Max output tokens |
|---|---|---|---|---|
| `fast` | chat | Quick answers, extraction, drafting, high volume | Off | 32,768 |
| `deep` | chat | Harder problems that benefit from step-by-step reasoning | On | 131,072 |

Both models use the same request format. Choosing the model is how you choose whether the model thinks first: `deep` returns its reasoning in a separate `reasoning` field and reports reasoning tokens in `usage`.

Your plan may set a lower maximum output (see [Limits](#limits)).

### List models

`GET /v1/models` returns the models your key may use, with their capabilities and prices.

```bash
curl https://api.innoai.net/v1/models -H "Authorization: Bearer $INNOAI_API_KEY"
```

```json
{
  "object": "list",
  "data": [
    {
      "id": "deep", "object": "model", "owned_by": "innoai", "type": "chat",
      "served_model": "InnoAI Deep 2026-09",
      "description": "Harder problems that benefit from step-by-step reasoning",
      "context_tokens": 262144,
      "max_output_tokens": 131072,
      "max_request_bytes": 20971520,
      "modalities": {"input": ["text"], "output": ["text"]},
      "reasoning": true,
      "unit": "tokens",
      "pricing": {
        "usd": {"input_per_1m": "10.000000", "output_per_1m": "20.000000", "reasoning_per_1m": "20.000000", "per_request": "0.000000"},
        "credits": {"input_per_1k": 2.0, "output_per_1k": 6.0, "reasoning_per_1k": 6.0, "per_request": 0.0}
      }
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `type` | Model type; decides which endpoint accepts it (see [Endpoints](#endpoints)) |
| `served_model` | Public label of the model that actually answers. Also sent as the `x-served-model` response header; quote it with `x-request-id` on receipts |
| `context_tokens` | Context window in tokens, `null` if not published |
| `max_output_tokens` | Largest `max_tokens` you can get: the model's cap, narrowed by your plan |
| `max_request_bytes` | Request body limit for this model |
| `modalities` | Input and output modalities (`text`, `audio`, `embedding`) |
| `reasoning` | Whether the model thinks before answering (reasoning tokens are billed at their own rate) |
| `unit` | What usage is metered in: `tokens`, `audio seconds` or `characters` |
| `pricing.usd` | Pay-as-you-go prices in USD, as 6-decimal strings. Keys depend on the type: per 1M tokens (`input_per_1m`, `output_per_1m`, `reasoning_per_1m`), `audio_per_minute` for speech-to-text, `chars_per_1m` for text-to-speech, plus `per_request` |
| `pricing.credits` | Subscription-plan credit prices per 1K tokens / per audio second / per 1K characters, plus `per_request` |

The list only contains models your key may use, so two keys can see different lists.

## Endpoints

| Method | Path | Model type | Status |
|---|---|---|---|
| POST | `/v1/chat/completions` | chat | Available (`fast`, `deep`) |
| POST | `/v1/completions` | chat or completion | Available (`fast`, `deep`) |
| GET | `/v1/models` | – | Available |
| POST | `/v1/embeddings` | embedding | Coming soon |
| POST | `/v1/rerank` | rerank | Coming soon |
| POST | `/v1/decide` | decision | Coming soon |
| POST | `/v1/entities` | entity extraction | Coming soon |
| POST | `/v1/classify` | classification | Coming soon |
| POST | `/v1/guard` | safety guard | Coming soon |
| POST | `/v1/audio/transcriptions` | speech-to-text | Coming soon |
| POST | `/v1/audio/speech` | text-to-speech | Coming soon |

Each endpoint accepts only models of its type. Sending a chat model to `/v1/embeddings`, for example, returns `400` with "model does not support this endpoint". "Coming soon" endpoints are live in the API but no model of that type is offered yet; they appear in `/v1/models` when one is.

Every request body must be a JSON object with a `model` field (for audio uploads, a `model` form field). The maximum request size is 20 MB.

### Chat completions

`POST /v1/chat/completions`

**Request**

```json
{
  "model": "fast",
  "messages": [
    {"role": "system", "content": "You are concise."},
    {"role": "user", "content": "Say hello in five words."}
  ],
  "max_tokens": 40
}
```

**Response** (`fast`)

```json
{
  "id": "chatcmpl-01a13230-1197-7034-898d-cba9d33c008a",
  "object": "chat.completion",
  "created": 1791843373,
  "model": "fast",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": "Greetings to you, my friend.", "reasoning": null},
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 15,
    "completion_tokens": 8,
    "total_tokens": 23,
    "completion_tokens_details": {"reasoning_tokens": 0}
  }
}
```

**Response** (`deep`): the thinking is in `message.reasoning`, the answer in `message.content`. Reasoning tokens are included in `completion_tokens` and broken out in `completion_tokens_details.reasoning_tokens`.

```json
{
  "id": "chatcmpl-01a13230-129d-74b1-8711-e2ba20f89ce2",
  "object": "chat.completion",
  "model": "deep",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": "12 × 12 = **144**", "reasoning": "12 × 12 = 144"},
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 26,
    "total_tokens": 42,
    "completion_tokens_details": {"reasoning_tokens": 11}
  }
}
```

#### Streaming

Set `"stream": true` to receive server-sent events. With `deep`, reasoning arrives first as `delta.reasoning`, then the answer as `delta.content`. The last event before `data: [DONE]` always carries `usage` (with an empty `choices` list): InnoAI turns on `stream_options.include_usage` for every streamed request.

```bash
curl -N https://api.innoai.net/v1/chat/completions \
  -H "Authorization: Bearer $INNOAI_API_KEY" -H "Content-Type: application/json" \
  -d '{"model": "deep", "stream": true, "messages": [{"role": "user", "content": "What is 2+2?"}]}'
```

```
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"deep","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"deep","choices":[{"index":0,"delta":{"reasoning":"Simple"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"deep","choices":[{"index":0,"delta":{"content":"2 + 2 = **4**"},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"deep","choices":[],"usage":{"prompt_tokens":14,"completion_tokens":14,"total_tokens":28,"completion_tokens_details":{"reasoning_tokens":3}}}

data: [DONE]
```

Python:

```python
stream = client.chat.completions.create(model="deep", stream=True,
    messages=[{"role": "user", "content": "What is 2+2?"}])
for chunk in stream:
    if chunk.choices:
        delta = chunk.choices[0].delta
        print(getattr(delta, "reasoning", None) or delta.content or "", end="")
    elif chunk.usage:
        print("\n", chunk.usage)
```

### Completions

`POST /v1/completions` takes a `prompt` instead of `messages` and works with `fast` and `deep`.

```json
{"model": "fast", "prompt": "The capital of France is", "max_tokens": 8}
```

```json
{
  "id": "cmpl-01a13230-141f-72de-9409-93faa476b80d",
  "object": "text_completion",
  "model": "fast",
  "choices": [{"index": 0, "text": " one of the most popular tourist destinations in", "finish_reason": "length"}],
  "usage": {"prompt_tokens": 5, "completion_tokens": 8, "total_tokens": 13}
}
```

### Embeddings, rerank, decide, entities, classify, guard

*Coming soon.* These endpoints accept a JSON object with a `model` of the matching type; the request and response formats will be documented here when a model is available. Embeddings will follow the OpenAI embeddings format.

### Audio transcriptions (speech-to-text)

*Coming soon.* `POST /v1/audio/transcriptions` will take `multipart/form-data` with the audio file and a `model` field, as in the OpenAI API. Usage is metered in seconds of audio.

### Audio speech (text-to-speech)

*Coming soon.* `POST /v1/audio/speech` will take JSON with `model` and `input` (the text), as in the OpenAI API, and stream back audio. Usage is metered in characters of `input`.

## Parameters InnoAI sets for you

- **Model name.** You always use the InnoAI model name (`fast`, `deep`); responses echo it.
- **Thinking.** Thinking is set by the model you choose: off for `fast`, on for `deep`. Any thinking setting you send is overridden.
- **Maximum output.** `max_tokens` and `max_completion_tokens` are clamped to the model's maximum (32,768 for `fast`, 131,072 for `deep`) and to your plan's maximum, if it has one. If you send neither, `max_tokens` is set to the applicable maximum. Set `max_tokens` yourself to keep responses (and costs) bounded.
- **Streaming usage.** Streamed requests always get a final usage event (see [Streaming](#streaming)).

## Limits

Limits come from your plan (and any narrower settings your administrator applies to your account or key). The dashboard shows your current limits and usage.

| Limit | How it works | When exceeded |
|---|---|---|
| Requests per minute | Rolling one-minute window | `429` `rate_limit_error`, with `Retry-After` |
| Tokens per minute / per day | Input + output tokens, counted when a request completes | `429` `rate_limit_error`, with `Retry-After` |
| Concurrent requests | Your requests in flight at once | `429` `rate_limit_error` "too many concurrent requests", `Retry-After: 1` |
| Max output tokens | Output is clamped (see above) | Not an error |
| Session budget | Credits per session. A session starts with your first request and lasts 5 hours; the next request after it ends starts a new one | `429` `usage_limit_error`, `limit: "session"` |
| Weekly budget | Credits used in the last 7 days (rolling, hourly granularity) | `429` `usage_limit_error`, `limit: "weekly"` |
| Monthly cap | Credits per calendar month in your account's time zone | `429` `usage_limit_error`, `limit: "monthly"` |
| Daily / monthly spend cap (pay-as-you-go) | USD spent today / this calendar month, if your account sets a cap | `429` `usage_limit_error`, `limit: "daily_spend"` / `"monthly_spend"` |
| Prepaid balance (pay-as-you-go) | Balance must be above zero (or your account's minimum) | `402` `insufficient_balance` |
| Model capacity | Shared capacity across all customers. When a model is busy, your request waits in a queue (currently up to 10 seconds) | `429` `capacity_error`, `Retry-After: 5` |

Usage-limit errors include `resets_at` (UTC, RFC 3339) and a `Retry-After` header with the seconds until then:

```json
{"error": {"message": "session usage limit reached for your plan (resets at the time in resets_at)",
           "type": "usage_limit_error", "limit": "session", "resets_at": "2026-10-13T03:16:13Z"}}
```

Budgets are checked before a request starts and charged when it finishes, so a request that starts within budget always completes; the counters can briefly go past the limit by the cost of requests already in flight.

## Response headers

| Header | When | Meaning |
|---|---|---|
| `x-request-id` | Requests that reached a model | Unique request id. Quote it when contacting support. |
| `x-served-model` | Requests that reached a model | Public label of the model that produced the response (same as `served_model` in `/v1/models`). The body's `model` field still echoes the name you requested. |
| `x-ratelimit-session-limit` / `-remaining` / `-reset` | Plans with a session budget | Session budget in credits, credits remaining, and when the session ends (UTC). |
| `x-ratelimit-weekly-limit` / `-remaining` / `-reset` | Plans with a weekly budget | Weekly budget, remaining, and when credit is next returned (UTC; the current time if nothing has been used). |
| `x-ratelimit-monthly-limit` / `-remaining` / `-reset` | Plans with a monthly cap | Monthly cap, remaining, and when the month resets (UTC). |
| `x-balance-remaining-usd` | Pay-as-you-go accounts | Available balance in USD (6 decimals) when the request was admitted, before its own cost. |
| `Retry-After` | `429` responses | Seconds to wait before retrying. |

Credit values in headers have up to 3 decimals, e.g. `x-ratelimit-session-remaining: 49999.961`.

## Billing and usage

Each account is on one of two billing types.

**Subscription plans** have a flat price and credit budgets (session, weekly and/or monthly). Each request costs credits based on what it used:

- **Chat and completion models:** a rate per 1,000 input tokens, per 1,000 output tokens, and per 1,000 reasoning tokens (reasoning tokens are charged at the reasoning rate instead of the output rate).
- **Speech-to-text:** per second of audio. **Text-to-speech:** per 1,000 characters of input.
- Some models may also have a per-request charge.

`deep` costs more credits per token than `fast`.

**Pay-as-you-go** accounts have a prepaid USD balance. Each completed request is debited at the model's USD prices (per 1M input, output and reasoning tokens; per minute of audio; per 1M characters; per request), exact to $0.000001. Requests are refused with `402` when the balance runs out; the request that crosses zero may take the balance slightly below zero by at most its own cost. Your administrator adds credit; the dashboard shows your balance, spend this month, cost per model and recent charges.

**How usage is measured:** tokens are the counts reported by the model in `usage`. If a stream ends before usage is reported (for example, you disconnect), the request is still charged: input is estimated from the request size and output as one token per event received. Requests rejected before reaching a model (`400`, `401`, `402`, `404`, `413`, `429`) are not charged, and neither are requests the model rejects with an error. A stream that breaks partway is charged for what was produced.

## Errors

Errors from InnoAI have this shape:

```json
{"error": {"message": "rate limit exceeded", "type": "rate_limit_error"}}
```

Messages never repeat any part of your request.

| Status | `type` | Meaning | What to do |
|---|---|---|---|
| 400 | `invalid_request_error` | Body is not a JSON object, `model` is missing, or the model does not support this endpoint | Fix the request |
| 401 | `authentication_error` | Missing, invalid, revoked, disabled or expired key | Check the key; create a new one if needed |
| 402 | `insufficient_balance` | Pay-as-you-go balance used up. Includes `balance_usd` | Ask your administrator to add credit |
| 403 | `billing_not_allowed` | The key is restricted to a billing source your account is not set up for | Ask your administrator |
| 404 | `not_found_error` | Unknown model, or not allowed for this key | Use a model from `GET /v1/models` |
| 404 | `not_found_error` | Unknown endpoint path ("unknown endpoint") | Check the path against the [Endpoints](#endpoints) table |
| 413 | `invalid_request_error` | Request body too large for this model | Send a smaller body |
| 429 | `rate_limit_error` | Requests/tokens per minute, tokens per day, or concurrency exceeded | Wait `Retry-After` seconds, then retry |
| 429 | `usage_limit_error` | Plan budget or spend cap reached; `limit` is `session`, `weekly`, `monthly`, `daily_spend` or `monthly_spend` | Wait until `resets_at`, or change plan |
| 429 | `capacity_error` | The model is at capacity right now | Retry after `Retry-After` (5 s) with backoff |
| 500 | `api_error` | Internal error | Retry with backoff |
| 502 | `api_error` | Model unavailable | Retry with backoff |
| 504 | `api_error` | Model timed out (no data for 300 seconds) | Retry; prefer streaming for long outputs |

If the model itself rejects a request (for example, an invalid message format), its error is passed through with its own status code; these follow the OpenAI error format. Internal detail (hosts, paths, stack traces) is removed; a backend fault that would only expose its internals is reported as "The model rejected the request (HTTP nnn)". If you close the connection before a response finishes, InnoAI records the request as client-closed (status 499 in your usage) and charges what was used up to that point.

## Privacy

- **No content logging or storage.** InnoAI does not log or store the content of requests or responses: no prompts, messages, completions, audio or text-to-speech input. Content passes through memory only, for the duration of the request.
- **Content-free usage records.** For billing and limits InnoAI records per request: time, account, user, key, model, status, whether it streamed, token counts (input, output, reasoning), request size in bytes, duration and cost. Never content.
- **Keys** are stored only as hashes.
- Usage records are retained according to your account's retention setting; aggregated hourly totals are kept for reporting.

## Best practices

- **Retry with backoff.** Retry `429`, `500`, `502` and `504` with exponential backoff and jitter (e.g. 1 s, 2 s, 4 s, up to about 30 s). Always honour `Retry-After` when present. Do not retry `400`, `401`, `402`, `404` or `413` unchanged. For `usage_limit_error`, wait until `resets_at`.
- **Stream long responses,** especially with `deep`. You see output immediately and long generations are not cut off by the 300-second idle timeout.
- **Set timeouts.** Use a connect timeout of about 10 s and a read timeout of at least 300 s for non-streaming `deep` requests. The OpenAI SDKs accept a `timeout` option.
- **Bound output** with `max_tokens` to control latency and cost.
- **Respect concurrency.** Keep in-flight requests at or below your plan's concurrency; queue the rest client-side.
- **Watch the headers.** `x-ratelimit-*-remaining` and `x-balance-remaining-usd` tell you when you are close to a limit.
- **Log `x-request-id`** with your own request logs to make support quick.

## Changelog

- **2026-10-16** — `GET /v1/models` now carries capabilities and prices (`served_model`, `context_tokens`, `max_output_tokens`, `modalities`, `reasoning`, `pricing`). New `x-served-model` response header. Unknown paths return a JSON `404 not_found_error`.

- **2026-10-12** — Pay-as-you-go billing: prepaid USD balance, `402 insufficient_balance`, `x-balance-remaining-usd`, daily/monthly spend caps. `system_fingerprint` is now always `null`.
- **2026-10-12** — `deep` model (reasoning on, 131,072 max output). Plans with session, weekly and monthly credit budgets and `x-ratelimit-*` headers. Shared model capacity with queueing and `capacity_error`.
- **Initial release** — OpenAI-compatible API with `fast`.
