# System One

Ask typed questions about a piece of text, data, an image, audio or video and get one typed answer per question. The Clairic model extracts values instead: you list typed fields and get one typed value per field. System One models are decision and extraction models, not chat models. Because the request is not a chat request, this is kenari's own contract and no OpenAI or Anthropic SDK method maps to it.

`POST /v1/systemone`

Only System One models are served here. Calling it with a chat model returns `400`, and calling a chat endpoint with a System One id returns `400` that points here. List the current System One models with a public call that needs no key:

```bash
curl "https://kenari.id/v1/models?modality=systemone"
```

A plain `GET /v1/models` does not list embedding, rerank, moderation or System One models. It does list chat, image, speech, transcription and video models. The examples below use free models. Swap in an id the call above returns, as long as the model takes what you send. There is no streaming.

## Request

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | yes | A System One model id. |
| `state` | string, object or array | yes | The text or data to evaluate. A `fields` call can leave it out when it sends an image. |
| `questions` | object | yes, or `fields` | One or more questions. Each key is a question id you choose, and the answer comes back under the same key. Up to 128 questions. |
| `fields` | object | yes, or `questions` | Clairic only. The typed fields to extract, in place of `questions`. Send `questions` or `fields`, not both. See [Extract fields](#extract-fields). |
| `reference_date` | string | no | Clairic only. An ISO date, such as `2026-10-25`, that anchors dates written without a year. |
| `options` | object | no | Clairic only. `include_quantiles` adds 99 percentiles to estimates. `return_state_text` returns the text that the `span` positions in the answers refer to, as `state_text`. Both are booleans. |
| `assets` | object | no | Images, each under a reference name you choose. See [Images](#images). |
| `images` | array | no | Images in order, for the models that take this shape. Send it or `assets`, not both. |
| `audio` | array | no | Audio clips, for the models that read audio. See [Audio and video](#audio-and-video). |
| `videos` | array | no | Videos, for the models that read video. See [Audio and video](#audio-and-video). |

The request body can be up to 512 KiB, not counting image, audio and video data. The whole body, that data included, can be up to 32 MiB, and a larger one is rejected with `413`. Base64 adds about a third to the size of a file. Each question is an object:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | yes | `noul`, `choice`, `score` or `box`. |
| `instructions` | string, object or array | yes | The question itself. |
| `criteria` | object or array | no | For `noul`, true and false descriptions. For `choice`, a map of the options. For `score`, an ordered rubric. `box` takes none. |

## Response

The response contains only `model`, `answers` and `usage`, plus `truncated` on a Clairic call and `state_text` when you ask for it. `model` is the versioned id that answered, and falls back to the id you requested. `answers` has one entry per question id, or one per field name for a `fields` call. `usage` reports `input_tokens` and `output_tokens`. A Clairic response is shown under [Extract fields](#extract-fields).

```json
{
  "model": "clef:free",
  "answers": {
    "q": { "type": "noul", "noul": 0.993 }
  },
  "usage": { "input_tokens": 47, "output_tokens": 0 }
}
```

## Images

Some System One models also read images. They list `image` in `modalities.input` in the `?modality=systemone` listing. Sending an image or a `box` question to any other model returns `400`.

Put each image in `assets` under a reference name you choose. Then write that name where the image belongs: in `state`, in a question's `instructions` or `criteria`, or in a field's `instructions`. The name can be the whole string or sit inside text. Object keys and the question `type` are never read as references.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | no | `image`. |
| `format` | string | yes | `png`, `jpg` or `jpeg`. It must match the image bytes. |
| `data_base64` | string | yes | The image file in standard base64, without a `data:` prefix. |

The fields below go in the request next to `model`, with an id from that listing:

```json
{
  "assets": {
    "receipt": { "type": "image", "format": "png", "data_base64": "iVBORw0KGgo..." }
  },
  "state": "receipt",
  "questions": {
    "itemized": {
      "type": "noul",
      "instructions": "Does receipt list a price for every item?"
    },
    "total": { "type": "box", "instructions": "The total amount in receipt" }
  }
}
```

A request carries up to 16 images, each at most 16 MiB. A reference name is 1 to 1,024 bytes, and all names together at most 65,536 bytes. Each model also has its own limits on how many images it reads, their size, their pixel dimensions and their file type. A request outside them returns `400` with a message that names the limit.

Some models also take an `images` array instead of `assets`. Each item is a `data:image/png;base64,...` URL, with `image/png`, `image/jpeg` or `image/webp`, or an object with `content_type` and `base64`. The model reads the images in array order. Send `assets` or `images`, not both. A model that reads only `assets` returns `400` for `images`.

### Box questions

A `box` question asks where something is in an image. It takes no `criteria`, and the request needs at least one image. The answer gives each edge of the box as a fraction of the image height or width, from 0 to 1, plus a confidence. Not every image model answers `box` questions.

```json
{ "type": "box", "box": { "top": 0.62, "bottom": 0.7, "left": 0.55, "right": 0.9 }, "confidence": 0.93 }
```

## Audio and video

Clef Omni reads audio and video as well as text and images. Models that read them list `audio` or `video` in `modalities.input` in the `?modality=systemone` listing. Sending audio or video to any other model returns `400`.

Send audio in `audio` and video in `videos`, next to `model`, `state` and `questions`. `state` is still required, so add a short line of text about what you send. Each item is a `data:` URL with base64 content, or an object with `content_type` and `base64`. kenari does not fetch links, so a URL returns `400`.

| Field | Limit | Types |
| --- | --- | --- |
| `audio` | Up to 4 clips, 8 MiB each | `audio/wav` (also `audio/x-wav` and `audio/wave`) or `audio/mpeg` (also `audio/mp3`) |
| `videos` | Up to 2 videos, 16 MiB each | `video/mp4` or `video/webm` |

Audio and video together can be at most 16 MiB per request. Sizes count the file, not its base64 text. The file must be what its type says, a WAV or MP3 for audio and an MP4 or WebM for video, or the request returns `400`.

```bash
curl https://kenari.id/v1/systemone \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "clef-omni:free",
    "state": "A voice note and a short video from the front door.",
    "audio": ["data:audio/wav;base64,UklGRiQAAABXQVZF..."],
    "videos": [{ "content_type": "video/mp4", "base64": "AAAAIGZ0eXBpc29t..." }],
    "questions": {
      "delivered": {
        "type": "noul",
        "instructions": "Was a parcel left at the door?"
      }
    }
  }'
```

The base64 in this example is cut short, so send the whole file. The response has the same shape as for any call with questions.

## Extract fields

Clairic reads a text, and images if you send them, and extracts one typed value per field. Send `fields` in place of `questions`, and send exactly one of the two. Each key in `fields` is a field name you choose, and the answer comes back under the same name. `state` is the text to read. An object or an array is sent on as its JSON text. It can be left out only when the request sends an image, and otherwise it must not be empty.

Each field is an object with a `type` and an optional `instructions` string that says what the field should hold:

| Type | Returns |
| --- | --- |
| `text` | A piece of the state, copied as written. |
| `number` | A number read or worked out from the state. |
| `date`, `time`, `datetime` | A calendar date, a time of day or both, in ISO form. |
| `duration` | A length of time. |
| `list` | Every matching text, number or date, in the order they appear. |
| `estimate` | An estimate of a number or date between the `min` and `max` you set. |
| `forecast` | One estimate for each future step of a series. |

A field name is 1 to 64 characters from A to Z, a to z, 0 to 9, underscore, dot and hyphen, and starts with a letter or underscore. A request holds 1 to 64 fields. kenari sends each field on as you wrote it, so a field also takes the settings of its type, such as `unit`, `integer` and `decimals` for a number, `items` for a list, `min`, `max`, `step`, `scale` and `null_threshold` for an estimate or a forecast, and `horizon` and `interval` for a forecast.

```bash
curl https://kenari.id/v1/systemone \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "clairic:free",
    "state": "Invoice INV-2291, issued 3 March 2026. Total due: Rp 4,250,000.",
    "fields": {
      "invoice_number": { "type": "text", "instructions": "The invoice number" },
      "total": { "type": "number", "instructions": "The total due in rupiah" }
    }
  }'
```

The response keeps `model`, `answers` and `usage`, with one answer per field name. It adds `truncated`, which is `true` when the state was cut to fit the model, and `state_text` when you set `options.return_state_text` to `true`.

```json
{
  "model": "clairic:free",
  "answers": {
    "invoice_number": {
      "type": "text",
      "value": "INV-2291",
      "guess": "INV-2291",
      "confidence": 0.999997,
      "p_null": 0.000056,
      "span": [8, 16]
    },
    "total": {
      "type": "number",
      "value": 4250000,
      "guess": 4250000,
      "confidence": 0.99999,
      "p_null": 0.000019
    }
  },
  "truncated": false,
  "usage": { "input_tokens": 71, "output_tokens": 0 }
}
```

| Key | Meaning |
| --- | --- |
| `type` | The field's type. |
| `value` | The answer, or `null` when the input does not determine it. |
| `guess` | The best candidate. It stays in the answer when `value` is `null`. |
| `confidence` | How sure the model is of the answer. |
| `p_null` | The probability that the input does not determine the value. |
| `span` | For a `text` field, the start and end positions of `guess` in the text that was read. `state_text` returns that text. |

A `list` field adds `spans`, and an `estimate` field adds `median`, `p10` and `p90`. Every value is well formed for its type, but not always correct, so use `confidence` and `p_null` to decide which answers to check by hand.

## Examples

### curl

```bash
curl https://kenari.id/v1/systemone \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "clef:free",
    "state": "a cat sitting on a rug",
    "questions": {
      "q": {
        "type": "noul",
        "instructions": "Is this a sentence about an animal?"
      }
    }
  }'
```

### Python

There is no SDK method for this endpoint, so use `requests`.

```python
import os
import requests

resp = requests.post(
    "https://kenari.id/v1/systemone",
    headers={"Authorization": f"Bearer {os.environ['KENARI_API_KEY']}"},
    json={
        "model": "clef:free",
        "state": "a cat sitting on a rug",
        "questions": {
            "q": {
                "type": "noul",
                "instructions": "Is this a sentence about an animal?",
            }
        },
    },
)
print(resp.json()["answers"])
```

### JavaScript

```js
const resp = await fetch("https://kenari.id/v1/systemone", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KENARI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "clef:free",
    state: "a cat sitting on a rug",
    questions: {
      q: {
        type: "noul",
        instructions: "Is this a sentence about an animal?",
      },
    },
  }),
});
const { answers } = await resp.json();
```

## Billing

System One is billed per input token. Output tokens are reported and not billed. Images, audio and video count as input tokens. The balance held for a request counts tokens for each image from its pixel size, once for every place its reference name appears. For each audio clip and each video it counts a fixed worst case, because kenari does not read how long a clip is. A free model such as `clef:free` charges Rp 0. See [How billing works](/en/docs/billing) for how the estimate, the reservation and the price dimensions work.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `bad_request` | `model` is unknown or is not a System One model, `state` is missing or is not a string, object or array, `questions` and `fields` are both sent or both missing, `questions` is empty or has more than 128 entries, a question has an unknown `type` or no `instructions`, or the body is over 512 KiB without image, audio and video data. |
| 400 | `bad_request` | The model does not read images, an image is not valid base64 or does not match its `format`, a `box` question has `criteria` or the request has no image, or the request is over the model's image limits. |
| 400 | `bad_request` | The model does not read audio or video, an `audio` or `videos` item is a URL or is not valid base64, its type is not an accepted one or does not match the file, or the request is over the clip limits. |
| 400 | `bad_request` | `fields` is sent to a model other than Clairic, or `questions` is sent to Clairic. A field name or `type` is not valid, `fields` has more than 64 entries, `reference_date` is not a string, an option is not `true` or `false`, or `state` is empty and no image is sent. |
| 400 | `model_not_found` | The model is listed but no provider serves it right now. Retry later. |
| 413 | none | The request body is above 32 MiB. The response is plain text. |

See [Errors](/en/docs/errors) for the shared codes, such as `all_providers_failed`.

## See also

- [Models and pricing](/en/docs/models)
- [How billing works](/en/docs/billing)
- [Free models](/en/docs/free-models)
