> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.empiriolabs.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.empiriolabs.ai/_mcp/server.

# Decisions API

Decision models answer typed questions about content you send. You send a `state` (text, JSON, images, audio, or video) and a set of `questions`, and each answer comes back as probabilities with a confidence value. Decision models never generate text.

Decision models are available through the API at `POST /v1/decisions`, and `aplomb-1` also answers the [chat formats](#chat-formats): OpenAI Chat Completions and Responses, Anthropic Messages, and Google Gemini.

## Endpoint

```text
POST https://api.empiriolabs.ai/v1/decisions
```

Authenticate with your API key in the `Authorization` header:

```text
Authorization: Bearer $EMPIRIOLABS_API_KEY
```

The full request and response schema is in the [API reference](/api-reference/api-reference/decisions/create-decision).

## Models

| Model                          | Input                            | Context                |
| ------------------------------ | -------------------------------- | ---------------------- |
| [`aplomb-1`](/models/aplomb-1) | Text, JSON, images, audio, video | Up to 1,000,000 tokens |

`aplomb-1` accepts every input type in the same state and supports the [not-in-the-state answer](#not-in-the-state-abstain).

## Your first request

This request asks three questions about one support message: whether it is urgent, which team should handle it, and how frustrated the customer is.

**`cURL`**

```bash title="cURL"
curl https://api.empiriolabs.ai/v1/decisions \
  -H "Authorization: Bearer $EMPIRIOLABS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "aplomb-1",
    "state": {
      "channel": "email",
      "message": "I was charged twice for my subscription this month. This is the third time I have written about it. Please refund the duplicate charge today."
    },
    "questions": {
      "is_urgent": {
        "type": "noul",
        "instructions": "Does the customer need a resolution today?"
      },
      "department": {
        "type": "choice",
        "instructions": "Which team should handle this message?",
        "criteria": {
          "billing": "Charges, refunds, and invoices",
          "technical": "Bugs, errors, and outages",
          "sales": "Plans, pricing, and upgrades"
        }
      },
      "frustration": {
        "type": "score",
        "instructions": "How frustrated is the customer?",
        "criteria": ["Calm", "Frustrated", "Very angry"]
      }
    }
  }'
```

**`Python`**

```python title="Python"
import os
import requests

response = requests.post(
    "https://api.empiriolabs.ai/v1/decisions",
    headers={"Authorization": f"Bearer {os.environ['EMPIRIOLABS_API_KEY']}"},
    json={
        "model": "aplomb-1",
        "state": {
            "channel": "email",
            "message": (
                "I was charged twice for my subscription this month. "
                "This is the third time I have written about it. "
                "Please refund the duplicate charge today."
            ),
        },
        "questions": {
            "is_urgent": {
                "type": "noul",
                "instructions": "Does the customer need a resolution today?",
            },
            "department": {
                "type": "choice",
                "instructions": "Which team should handle this message?",
                "criteria": {
                    "billing": "Charges, refunds, and invoices",
                    "technical": "Bugs, errors, and outages",
                    "sales": "Plans, pricing, and upgrades",
                },
            },
            "frustration": {
                "type": "score",
                "instructions": "How frustrated is the customer?",
                "criteria": ["Calm", "Frustrated", "Very angry"],
            },
        },
    },
)
answers = response.json()["answers"]
if answers["is_urgent"]["noul"] > 0.8:
    print("Urgent, route to", answers["department"]["choice"])
```

**`Response`**

```json title="Response"
{
  "model": "aplomb-1",
  "answers": {
    "is_urgent": {"type": "noul", "noul": 0.9412, "confidence": 0.8824},
    "department": {
      "type": "choice",
      "choice": "billing",
      "probabilities": {"billing": 0.88, "technical": 0.11, "sales": 0.01},
      "confidence": 0.82
    },
    "frustration": {
      "type": "score",
      "score": 1.05,
      "legend": {"0": "Calm", "1": "Frustrated", "2": "Very angry"},
      "probabilities": {"0": 0.02, "1": 0.91, "2": 0.07},
      "confidence": 0.865
    }
  },
  "usage": {
    "input_tokens": 212,
    "output_tokens": 0,
    "input_tokens_details": {"text_tokens": 212, "image_tokens": 0, "audio_tokens": 0, "video_tokens": 0}
  },
  "long_context": "full"
}
```

## Request

| Field          | Type                     | Required | Description                                                                                                               |
| -------------- | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------- |
| `model`        | string                   | Yes      | `aplomb-1`. An unknown ID returns `404 model_not_found`.                                                                  |
| `state`        | string, object, or array | Yes      | The content to decide about. Media nodes can appear anywhere inside it.                                                   |
| `questions`    | object                   | Yes      | 1 to 128 questions, keyed by an ID you choose.                                                                            |
| `long_context` | string                   | No       | `fast` (default) or `full`: how a state longer than 131,072 tokens is read. See [Long states](#long-states-long_context). |

**Question IDs.** Answers come back under the same IDs you send. IDs are not shown to the model, so name them for your own code. An ID cannot contain control characters.

**Instructions.** Every question has a `type` and `instructions`. `instructions` is usually a string. It can also be an object or an array that holds the question in one field and the data it refers to in others.

**Independent answers.** Each question is answered independently of the other questions in the request.

## Question types

### Yes or no (`noul`)

`noul` asks a yes/no question. The answer's `noul` field is the probability that the answer is yes. Add `criteria` with `true` and `false` descriptions when the line between yes and no needs defining:

```json
"refund_request": {
  "type": "noul",
  "instructions": "Is the customer asking for a refund?",
  "criteria": {
    "true": "The message asks for money back.",
    "false": "The message reports a problem without asking for money back."
  }
}
```

**`Answer`**

```json title="Answer"
"refund_request": {"type": "noul", "noul": 0.9721, "confidence": 0.9442}
```

### Choice (`choice`)

`choice` picks one of 2 to 255 named options. `criteria` maps each option name to a description, or to `null` when the name is enough. The answer's `choice` is the most likely option, and `probabilities` holds the probability of every option. Option names are returned unchanged.

A choice between exactly two yes/no options, such as `yes` and `no`, returns the same probability as the equivalent `noul` question, so the two never disagree.

### Score (`score`)

`score` rates the state on an ordered scale of 2 to 10 levels, listed lowest first in `criteria`. Levels are numbered from 0 in that order. The answer's `score` is the expected level (the probability-weighted average of the level numbers), `legend` maps each level number to its description, and `probabilities` holds the probability of each level.

In the first request above, `frustration` returned a `score` of 1.05, with most of the probability on level 1 (`Frustrated`).

### Tool (`tool`)

`tool` chooses which function to call. `tools` is an array of 1 to 254 function definitions in the same format as `tools` on [Chat Completions](/api-reference/api-reference/chat-completions/create-chat-completion).

* **Function choice.** The answer's `tool` is the most likely function, and `probabilities` holds the probability of each. Function names are returned unchanged, including `none`.
* **No function.** With `allow_none` set to `true` (the default), `tool` can be `null`. Its probability is `probabilities.none`, unless a supplied function is named `none`; in that case the no-function probability is returned separately as `none_probability`. The function probabilities and any separate `none_probability` sum to 1. With `allow_none: false`, only the supplied functions are candidates.
* **Arguments.** Every `enum` and `boolean` argument of every function gets its own distribution under `arguments`: which value the argument should take if that function is called. `enum` arguments return `probabilities` and `boolean` arguments return `probability_true`.
* **Open arguments.** Other arguments, such as free text, are listed under `open_arguments`. Decision models do not fill them; produce them separately if you need them.

**`Request`**

```json title="Request"
{
  "model": "aplomb-1",
  "state": "Customer: Can you move my Thursday appointment to Friday morning?",
  "questions": {
    "next_action": {
      "type": "tool",
      "instructions": "Which function should handle this request?",
      "tools": [
        {
          "type": "function",
          "function": {
            "name": "reschedule_appointment",
            "description": "Move an existing appointment to a new day and time.",
            "parameters": {
              "type": "object",
              "properties": {
                "day": {"type": "string", "enum": ["monday", "tuesday", "wednesday", "thursday", "friday"]},
                "time_of_day": {"type": "string", "enum": ["morning", "afternoon", "evening"]},
                "notify_by_sms": {"type": "boolean"},
                "note": {"type": "string"}
              },
              "required": ["day", "time_of_day"]
            }
          }
        },
        {
          "type": "function",
          "function": {
            "name": "cancel_appointment",
            "description": "Cancel an existing appointment.",
            "parameters": {"type": "object", "properties": {}}
          }
        }
      ]
    }
  }
}
```

**`Answer`**

```json title="Answer"
"next_action": {
  "type": "tool",
  "tool": "reschedule_appointment",
  "probabilities": {"reschedule_appointment": 0.96, "cancel_appointment": 0.01, "none": 0.03},
  "confidence": 0.94,
  "arguments": {
    "reschedule_appointment": {
      "day": {
        "value": "friday",
        "probabilities": {"monday": 0.001, "tuesday": 0.002, "wednesday": 0.007, "thursday": 0.04, "friday": 0.95},
        "confidence": 0.9375
      },
      "time_of_day": {
        "value": "morning",
        "probabilities": {"morning": 0.93, "afternoon": 0.05, "evening": 0.02},
        "confidence": 0.895
      },
      "notify_by_sms": {"value": false, "probability_true": 0.12, "confidence": 0.76}
    }
  },
  "open_arguments": {"reschedule_appointment": ["note"]}
}
```

A `tool` question counts once toward the 128-question limit. Its functions can have at most 64 `enum` and `boolean` arguments in total. A request can evaluate at most 512 questions in total, where each `tool` question counts as one plus one per `enum` or `boolean` argument.

## Not in the state (`abstain`)

Add `"abstain": true` to a `noul`, `choice`, or `score` question to let the model say that the state does not contain what the question needs. The answer then includes `abstain`, the probability of that outcome. Every other field is the answer given that the state does contain it, so `noul` is still the probability of yes and `probabilities` still sum to 1. A `choice` with `abstain` takes at most 254 options.

**`Request`**

```json title="Request"
{
  "model": "aplomb-1",
  "state": {"order": {"id": "B-44120", "status": "shipped", "payment": "unpaid"}},
  "questions": {
    "paid": {"type": "noul", "instructions": "Has order B-44120 been paid?", "abstain": true},
    "carrier": {
      "type": "choice",
      "instructions": "Which carrier is delivering order B-44120?",
      "criteria": {"ups": null, "fedex": null, "dhl": null},
      "abstain": true
    }
  }
}
```

**`Answers`**

```json title="Answers"
"paid": {"type": "noul", "noul": 3.47e-06, "confidence": 0.99999306, "abstain": 5.09e-06},
"carrier": {
  "type": "choice",
  "choice": "fedex",
  "probabilities": {"ups": 0.22482937, "fedex": 0.55308475, "dhl": 0.22208588},
  "confidence": 0.32962713,
  "abstain": 0.99998646
}
```

The record says the order is unpaid, so `paid` is a confident no with a low `abstain`. It does not name a carrier, so `carrier` has a high `abstain`: treat the choice as unanswered rather than acting on it.

## Confidence

Every answer includes `confidence`, from 0 to 1, which measures how concentrated the distribution is. 0 means every option is equally likely; 1 means all probability is on one option. For n options whose highest probability is p, confidence is `(n * p - 1) / (n - 1)`. For a `noul` answer it is `|2p - 1|`.

Use confidence as a threshold: act automatically on high-confidence answers and send the rest for review.

Probabilities are rounded to at most 8 decimal places and sum to 1.

## Media input

Put images, audio, and video anywhere inside `state` as media nodes. A media node is an object with a `type` of `image`, `audio`, or `video`, and either a public `https://` URL in `url`, or the file as base64 in `data` with its MIME type in `mime`:

```json
{"type": "image", "url": "https://example.com/listing-photo.png"}
{"type": "image", "data": "<base64>", "mime": "image/png"}
{"type": "audio", "url": "https://example.com/support-call.wav"}
{"type": "video", "url": "https://example.com/dashcam.mp4"}
```

**`Request with an image`**

```json title="Request with an image"
{
  "model": "aplomb-1",
  "state": {
    "listing_title": "Three-seat leather sofa, like new",
    "photo": {"type": "image", "url": "https://example.com/listing-photo.png"}
  },
  "questions": {
    "matches_title": {"type": "noul", "instructions": "Does the photo show the item described in listing_title?"},
    "condition": {"type": "score", "instructions": "Rate the condition of the item in the photo.", "criteria": ["Damaged", "Worn", "Good", "Like new"]}
  }
}
```

`aplomb-1` accepts images, audio, and video, in any mix, in the same state.

| Media | Per request | Per item              |
| ----- | ----------- | --------------------- |
| Image | Up to 16    | 20 MB                 |
| Audio | Up to 8     | 25 MB and 10 minutes  |
| Video | Up to 4     | 100 MB and 10 minutes |

A media URL must be publicly reachable over `https://`. Other URLs return `400 invalid_media_url`; send the file as `data` instead.

## Long states (`long_context`)

For a state longer than 131,072 tokens, `long_context` sets how the state is read:

* **`fast` (default).** The model answers in seconds: about 3 seconds for a 1M-token state, against about 111 seconds for a full read. On our long-context test sets (164 documents of 131K to 1M tokens, 525 decisions), fast mode answered every decision correctly, against 95.2% for a full read.
* **`full`.** The model reads every token of the state. Use it when an answer depends on the whole document, such as a count, the overall tone, or confirming that something never appears.

**Shorter states.** A state of 131,072 tokens or fewer is always read in full, whichever mode you send.

**Many questions.** A request that evaluates more than 8 questions is read in full. Each `tool` question counts as one plus one per `enum` or `boolean` argument, as in [Limits](#limits).

**Media.** Images, audio, and video in the state are read in full in both modes.

**Which mode was used.** The response's `long_context` field is `fast` or `full`. A `fast` request can also come back `full`.

**Usage.** The whole state is counted in both modes, at the same price.

**`Request`**

```json title="Request"
{
  "model": "aplomb-1",
  "state": "<the full text of a long contract>",
  "long_context": "full",
  "questions": {
    "auto_renews": {"type": "noul", "instructions": "Does the contract renew automatically?"}
  }
}
```

Any value other than `fast` or `full` returns `422 invalid_request`.

## Limits

* **Questions.** 1 to 128 per request. A `tool` question counts once toward that limit.
* **Evaluated questions.** At most 512 per request, where each `tool` question counts as one plus one per `enum` or `boolean` argument. More returns `422 too_many_questions`.
* **Tokens.** The state plus the longest question can be up to 1,000,000 tokens.
* **Choice options.** 2 to 255 per `choice` question.
* **Score levels.** 2 to 10 per `score` question.
* **Functions.** 1 to 254 per `tool` question, with at most 64 `enum` and `boolean` arguments in total.
* **Media.** See the table in [Media input](#media-input).

## Usage and billing

Decision requests are billed on input tokens only. `usage.input_tokens` counts what you send:

* The state, once per request, however many questions refer to it, and in full in both [`long_context`](#long-states-long_context) modes.
* The text of each question.
* Each media item: at most 640 tokens per image, at most 256 tokens per video frame, and about 13 tokens per second of audio.

A `tool` question counts its own `instructions` and `tools` once, however many arguments it evaluates.

Nothing else is added to the count, and `output_tokens` is always 0. `input_tokens_details` splits `input_tokens` into `text_tokens`, `image_tokens`, `audio_tokens`, and `video_tokens`.

Because the state is counted once per request, asking several questions about the same state in one request costs less than sending one request per question. Current rates are on each model page and on the [pricing page](https://empiriolabs.ai/pricing).

## Data retention

Zero data retention is on by default: we do not retain the content of your decision requests or responses.

## Errors

Errors return a JSON body with a `code` and a `message`. Validation messages name the offending field:

```json
{"error": {"code": "too_many_questions", "message": "At most 128 questions are accepted per request."}}
```

| Status | `code`                 | Meaning                                                                                                                 |
| ------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_media_url`    | A media URL was refused. Use a publicly reachable `https://` URL, or send the file as `data`.                           |
| 400    | `unsupported_media`    | The model does not accept a media type in the state.                                                                    |
| 401    | `missing_api_key`      | No API key was sent. Send it as `Authorization: Bearer <key>`.                                                          |
| 401    | `invalid_api_key`      | The API key is malformed, inactive, expired, or not found.                                                              |
| 402    | `insufficient_credits` | The account balance is too low for the request.                                                                         |
| 404    | `model_not_found`      | The `model` ID is unknown.                                                                                              |
| 413    | `request_too_large`    | The request body is too large. Send large media as `url` instead of `data`.                                             |
| 422    | `invalid_request`      | The request does not match the schema. The message names the field.                                                     |
| 422    | `state_too_large`      | The state plus the longest question is over 1,000,000 tokens.                                                           |
| 422    | `too_many_questions`   | The request has more than 128 questions, or evaluates more than 512.                                                    |
| 422    | `invalid_media`        | A media item is invalid or over a limit in [Media input](#media-input).                                                 |
| 429    | `rate_limit_exceeded`  | An account rate limit was reached. Retry after the number of seconds in `Retry-After`.                                  |
| 503    | `model_unavailable`    | The model is temporarily unavailable. Retry after the number of seconds in `Retry-After`; the request is safe to retry. |

See [Errors and Status Codes](/errors) for how the API reports errors across endpoints.

## Chat formats

`aplomb-1` also answers requests in the OpenAI Chat Completions, OpenAI Responses, Anthropic Messages, and Google Gemini formats, so you can call it from those SDKs and from tools built on them. Use the base URL `https://api.empiriolabs.ai/v1` (for Gemini clients, `https://api.empiriolabs.ai`) and the model ID `aplomb-1`.

A chat request becomes one decision about the whole conversation:

| Request                                                   | Question                                                                                                                                                                                          | Reply                                                                         |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Messages only                                             | The last user message, as a yes or no question                                                                                                                                                    | `Yes (84%)` or `No (91%)`                                                     |
| `answer_type` and `options` (extra body fields)           | The last user message, as a `choice` among `options`, or a `score` with `options` as the levels from lowest to highest (1 to 5 when empty). `options` takes an array or a comma-separated string. | `billing (97%)`, `4 out of 5 (62%)` or `Annoyed (96%)`                        |
| `tools`                                                   | Which function to call next                                                                                                                                                                       | A tool call with the most likely value of every `enum` and `boolean` argument |
| `response_format` with a JSON schema (structured outputs) | One question per property, from its `description`: a `boolean` is yes or no, an `enum` is a choice, and an `integer` with a `minimum` and `maximum` 2 to 10 values apart is a score               | A JSON object with each property's most likely value                          |
| `questions` (an extra body field)                         | Your questions, exactly as in `/v1/decisions`                                                                                                                                                     | The `answers` object, as JSON                                                 |

* Images, audio, and video in the messages are part of the state.
* Every reply carries the full answer, with probabilities, under `message.decision`. With `"logprobs": true`, a yes or no reply also returns the probabilities as log probabilities.
* A JSON schema property that also allows `null` (for example `"type": ["string", "null"]`, `"nullable": true`, or `null` in its `enum`) comes back `null` when the input doesn't hold the answer.
* Send `"abstain": true` with a yes or no request to allow the reply `The input doesn't say`.
* Free-text tool arguments are not included in the tool call, and a JSON schema property must be a `boolean`, an `enum`, or an `integer` range.
* Gemini `responseSchema` and the Responses API's `text.format` work the same way as `response_format`.
* Streaming is supported. Usage reports the input tokens as `prompt_tokens` and 0 completion tokens, billed the same as `/v1/decisions`.

```python
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.empiriolabs.ai/v1", api_key=os.environ["EMPIRIOLABS_API_KEY"])
reply = client.chat.completions.create(
    model="aplomb-1",
    messages=[{"role": "user", "content": "Has order B-44120 been paid? Order B-44120: status shipped, payment unpaid."}],
)
print(reply.choices[0].message.content)  # No (>99.9%)

triage = client.chat.completions.create(
    model="aplomb-1",
    messages=[{"role": "user", "content": "I was charged twice for order B-44120. Please refund the extra charge."}],
    response_format={"type": "json_schema", "json_schema": {"name": "triage", "schema": {"type": "object", "properties": {
        "team": {"type": "string", "enum": ["billing", "technical", "sales"], "description": "Which team should handle it?"},
        "anger": {"type": "integer", "minimum": 1, "maximum": 5, "description": "How upset is the customer, 1 to 5?"},
        "carrier": {"type": ["string", "null"], "enum": ["ups", "fedex", "dhl", None], "description": "Which carrier delivered it?"},
    }}}},
)
print(triage.choices[0].message.content)  # {"team": "billing", "anger": 2, "carrier": null}
```

Every Decisions API feature also works from a chat SDK: send a `/v1/decisions` question map as `questions`, for example `extra_body={"questions": {...}}` with the OpenAI SDK.

## MCP

MCP clients can call the Decisions API as the `decide` tool on the [EmpirioLabs MCP server](/mcp).