# Answer typed questions about a state

`POST /v1/decide` (operation id `decide`)

Answers every question against the shared state in one call. The state is billed once. Requests to an exact
GPU model version (for example `dex-1.0.0`) or with `fallback: "never"` are never served by the fallback path;
send `fallback: "never"` for content moderation, because the fallback's content filters block that content.
A body that repeats a key in any JSON object is rejected with 400 `duplicate_key`. See `docs/spec/dex-v1.md`
sections 5 to 7.

Authentication: API key as `authorization: Bearer <key>`, scope `decide`.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `idempotency-key` | header | string | no | Makes retries safe. Within 24 hours, a repeat with the same key and the same body is never charged again. It answers with `idempotent-replayed: true` and the original response: the same `id`, `created`, `model`, answers and `usage`, including the original charge fields. The same key with a different body answers 409.  (1 to 255 characters; pattern ^[\x21-\x7E]+$) |
| `x-client-request-id` | header | string | no | Your own correlation id, accepted on every public operation. Logged with the request metadata and echoed in the response. A malformed value answers 400 `invalid_header`. (1 to 128 characters; pattern ^[\x21-\x7E]+$) |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | ModelName | no | An alias such as `dex-1` or an exact version such as `dex-1.0.0`. (default "dex-1"; pattern ^dex-[0-9]+(\.[0-9]+\.[0-9]+)?$) |
| `state` | State | yes | The material the questions are about. A string, or JSON whose fields questions can point at with `{{path}}` references. A string of only whitespace, an empty object and an empty array are rejected (empty objects and arrays inside the state are fine). At most 16,384 tokens after rendering.  (string: 1 to 65,536 characters, pattern \S; object: at least 1 entries; array of any: at least 1 items) |
| `questions` | object | yes | Question id to question. Ids are for your code only and never reach the model. (1 to 32 entries; keys match ^[A-Za-z][A-Za-z0-9_-]{0,63}$) |
| `questions.<question id>` | Question | no |  |
| **When `type` is `pick`** | PickQuestion |  |  |
| `questions.<question id>.type` | string | yes | (always "pick") |
| `questions.<question id>.instructions` | Instructions | yes | What to decide; a string of only whitespace is rejected. May contain `{{path}}` references to state fields, such as `{{ticket.body}}`. (1 to 4,000 characters; pattern \S) |
| `questions.<question id>.criteria` | Criteria | no | Extra rules or definitions the model must apply. May contain `{{path}}` references. A string of only whitespace is rejected. `null` is the same as leaving the field out. (string: 1 to 4,000 characters, pattern \S; array of string: 1 to 20 items) |
| `questions.<question id>.options` | object | yes | Label to description (or null). Key order is kept and shown to the model in that order. Labels have no leading or trailing whitespace and are unique after Unicode NFC normalisation and lower-casing (`Billing` and `billing` are the same label). A description of only whitespace is rejected.  (1 to 255 entries; keys match ^\S(.*\S)?$, 1 to 100 characters; values: string \| null, 1 to 1,000 characters, pattern \S) |
| `questions.<question id>.options.<label>` | string \| null | no | (1 to 1,000 characters; pattern \S) |
| `questions.<question id>.min_confidence` | MinConfidence | no | When the answer's confidence is below this value, the answer carries `abstained` set to true. (0 to 1) |
| **When `type` is `rate` (beta)** | RateQuestion |  |  |
| `questions.<question id>.type` | string | yes | (always "rate") |
| `questions.<question id>.instructions` | Instructions | yes | What to decide; a string of only whitespace is rejected. May contain `{{path}}` references to state fields, such as `{{ticket.body}}`. (1 to 4,000 characters; pattern \S) |
| `questions.<question id>.criteria` | Criteria | no | Extra rules or definitions the model must apply. May contain `{{path}}` references. A string of only whitespace is rejected. `null` is the same as leaving the field out. (string: 1 to 4,000 characters, pattern \S; array of string: 1 to 20 items) |
| `questions.<question id>.levels` | array of string | yes | Ordered from lowest (index 0) to highest. A level follows the label rule (no leading or trailing whitespace), and levels are unique after Unicode NFC normalisation and lower-casing.  (2 to 10 items; unique items; each item: 1 to 200 characters, pattern ^\S(.*\S)?$) |
| `questions.<question id>.min_confidence` | MinConfidence | no | When the answer's confidence is below this value, the answer carries `abstained` set to true. (0 to 1) |
| **When `type` is `check`** | CheckQuestion |  |  |
| `questions.<question id>.type` | string | yes | (always "check") |
| `questions.<question id>.instructions` | Instructions | yes | What to decide; a string of only whitespace is rejected. May contain `{{path}}` references to state fields, such as `{{ticket.body}}`. (1 to 4,000 characters; pattern \S) |
| `questions.<question id>.criteria` | Criteria | no | Extra rules or definitions the model must apply. May contain `{{path}}` references. A string of only whitespace is rejected. `null` is the same as leaving the field out. (string: 1 to 4,000 characters, pattern \S; array of string: 1 to 20 items) |
| `questions.<question id>.min_confidence` | MinConfidence | no | When the answer's confidence is below this value, the answer carries `abstained` set to true. (0 to 1) |
| `fallback` | string | no | `never` keeps the request on the GPU path even when the model is an alias. Exact versions never use the fallback. Send `never` for content moderation: the fallback's content filters block that content. (one of "allow", "never"; default "allow") |

## Responses

### 200

All questions answered.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | (pattern ^req_[0-9A-HJKMNP-TV-Z]{26}$) |
| `object` | string | yes | (always "decision") |
| `created` | integer | yes | Unix time in seconds. |
| `model` | string | yes | The exact version that answered, such as `dex-1.0.0` or `dex-fallback-1.0.0`. |
| `served_by` | ServedBy | yes | `gpu` is our own engine; `fallback` is the hosted Azure OpenAI path in the EU data zone. (one of "gpu", "fallback") |
| `calibration` | string | yes | Calibration version applied; its public report shows the reliability curves. (pattern ^cal-[0-9]{8}-[0-9]+$) |
| `answers` | object | yes | Question id to answer, in the request's question order. |
| `answers.<question id>` | Answer | no |  |
| **When `type` is `pick`** | PickAnswer |  |  |
| `answers.<question id>.type` | string | yes | (always "pick") |
| `answers.<question id>.choice` | string | yes | The most probable label. Ties go to the earlier option. |
| `answers.<question id>.probabilities` | object | yes | Label to probability, in option order, summing to exactly 1. (values: Probability, 0 to 1) |
| `answers.<question id>.probabilities.<label>` | Probability | no | Rounded to 4 decimals. (0 to 1) |
| `answers.<question id>.confidence` | Confidence | yes | How concentrated the distribution is (see the spec, section 6.3). Not a correctness guarantee. (0 to 1) |
| `answers.<question id>.abstained` | boolean | yes |  |
| **When `type` is `rate` (beta)** | RateAnswer |  |  |
| `answers.<question id>.type` | string | yes | (always "rate") |
| `answers.<question id>.rating` | number | yes | Probability-weighted level index, from 0 to the number of levels minus 1. (0 to 9) |
| `answers.<question id>.levels` | array of string | yes | The levels you sent, in order. |
| `answers.<question id>.probabilities` | array of Probability | yes | Aligned with `levels`, summing to exactly 1. (each item: 0 to 1) |
| `answers.<question id>.confidence` | Confidence | yes | How concentrated the distribution is (see the spec, section 6.3). Not a correctness guarantee. (0 to 1) |
| `answers.<question id>.abstained` | boolean | yes |  |
| **When `type` is `check`** | CheckAnswer |  |  |
| `answers.<question id>.type` | string | yes | (always "check") |
| `answers.<question id>.probability` | Probability | yes | Rounded to 4 decimals. (0 to 1) |
| `answers.<question id>.confidence` | Confidence | yes | How concentrated the distribution is (see the spec, section 6.3). Not a correctness guarantee. (0 to 1) |
| `answers.<question id>.abstained` | boolean | yes |  |
| `usage` | Usage | yes |  |
| `usage.input_tokens` | integer | yes | Billed tokens, equal to state_tokens plus question_tokens. Output is free. (at least 1) |
| `usage.state_tokens` | integer | yes | (at least 1) |
| `usage.question_tokens` | integer | yes | (at least 1) |
| `usage.charge_micro_cents` | integer | yes | Debited from the balance. 1 EUR is 100,000,000 micro-cents. (format int64; at least 0) |
| `usage.unit_price_micro_cents` | integer | yes | Price per token for this request's tier (5 is EUR 0.05 per million tokens). (at least 0) |
| `usage.tier` | string | yes | (one of "t1", "t2", "t3", "test") |

### 400

The request could not be read. Codes: `invalid_json` (not JSON, not UTF-8, or a `\u` escape that is half of a
surrogate pair, such as `"\ud800"` without its low half), `duplicate_key` (a JSON object
repeats a key; `param` is the JSON pointer of the repeated member, such as `/questions/q1/options/a`),
`unsupported_media_type` (not `application/json`) and `invalid_header` (a malformed `idempotency-key` or
`x-client-request-id`; `param` names the header).


### 401

Missing, unknown, revoked or expired credentials.

### 402

The balance does not cover this request's charge. Nothing was charged.

### 403

The credentials are valid but not allowed to do this (missing scope, suspended account or failed CSRF check).

### 404

The model, key or route does not exist, or the model version is retired.

### 409

The idempotency key was used with a different body, or the first request with this key is still running.

### 413

The body or its token counts exceed the limits.

### 422

The request is well-formed JSON but breaks a validation rule. `param` names the field as a dotted path.
Codes: `unknown_field`, `missing_field`, `invalid_type` (wrong JSON type), `invalid_value` (right type, value
out of range: an empty or over-long string, a state, `instructions`, `criteria` string or option description
of only whitespace, an empty `questions` or `options` object, an empty state object or array, a pattern or
allowed-value mismatch such as a label or level with outer whitespace, a state nested deeper than 32 levels, a
bad date or date range), `field_not_allowed` (`options` or `levels` on the wrong question type),
`invalid_question_id`, `too_many_questions`, `too_many_options`, `invalid_levels` (including two levels equal
after Unicode NFC normalisation and lower-casing), `invalid_min_confidence`, `duplicate_label` (two option
labels equal after Unicode NFC normalisation and lower-casing, such as `Billing` and `billing`),
`state_path_not_found`, `state_not_json` and, on the console, `top_up_limit_exceeded`.


### 429

A rate limit or quota is exhausted. Retry after the given number of seconds. For `test_daily_quota` that is
the time until 00:00 UTC; clients should not retry it automatically.


### 500

An unexpected error. Any charge was refunded. Retry with the same idempotency key.

### 503

No serving path could answer in time, or a dependency is down. Any charge was refunded.

## Example request

```bash
curl -X POST "https://api.thinqit.ai/v1/decide" \
  -H "authorization: Bearer $DEX_API_KEY" \
  -H "content-type: application/json" \
  --data @- <<'JSON'
{
  "state": {
    "ticket": {
      "channel": "email",
      "subject": "Charged twice, still no answer",
      "body": "This is the third time I am writing. You charged my card twice for order 4471 and nobody replies. Fix it today or I cancel my subscription."
    }
  },
  "questions": {
    "team": {
      "type": "pick",
      "instructions": "Which team should handle {{ticket.body}}?",
      "options": {
        "billing": "Payments, refunds and invoices",
        "technical": "Bugs, outages and sign-in problems",
        "shipping": "Delivery and returns",
        "other": null
      },
      "min_confidence": 0.5
    },
    "urgency": {
      "type": "rate",
      "instructions": "How fast must we reply to {{ticket.body}}?",
      "levels": [
        "Can wait",
        "This week",
        "Within two working days",
        "Today"
      ]
    },
    "angry": {
      "type": "check",
      "instructions": "Is the writer of {{ticket.body}} angry?",
      "min_confidence": 0.6
    }
  }
}
JSON
```

## Examples

Illustrative values. Examples show the shape of requests and responses; the numbers in them are not measured results.

Request: The Quickstart's support ticket (pick, rate and check); the mock server answers it by default

```json
{
  "state": {
    "ticket": {
      "channel": "email",
      "subject": "Charged twice, still no answer",
      "body": "This is the third time I am writing. You charged my card twice for order 4471 and nobody replies. Fix it today or I cancel my subscription."
    }
  },
  "questions": {
    "team": {
      "type": "pick",
      "instructions": "Which team should handle {{ticket.body}}?",
      "options": {
        "billing": "Payments, refunds and invoices",
        "technical": "Bugs, outages and sign-in problems",
        "shipping": "Delivery and returns",
        "other": null
      },
      "min_confidence": 0.5
    },
    "urgency": {
      "type": "rate",
      "instructions": "How fast must we reply to {{ticket.body}}?",
      "levels": [
        "Can wait",
        "This week",
        "Within two working days",
        "Today"
      ]
    },
    "angry": {
      "type": "check",
      "instructions": "Is the writer of {{ticket.body}} angry?",
      "min_confidence": 0.6
    }
  }
}
```

Request: Dutch customer support triage (pick, rate and check on one ticket)

```json
{
  "model": "dex-1",
  "state": {
    "bericht": {
      "kanaal": "webformulier",
      "onderwerp": "Accu laadt niet meer op",
      "tekst": "Sinds de software-update van vorige week laadt de accu van mijn e-bike niet meer op. Ik heb de fiets pas drie maanden en heb hem elke dag nodig voor mijn werk. Kan ik hem omruilen of komt er een monteur langs?"
    },
    "klant": {
      "klant_sinds": "2026-06-14",
      "bestellingen": 2
    }
  },
  "questions": {
    "afdeling": {
      "type": "pick",
      "instructions": "Welke afdeling moet {{bericht.tekst}} oppakken?",
      "options": {
        "garantie": "Omruilen of terugbetalen binnen de garantietermijn",
        "reparatie": "Defecten, onderhoud en monteur aan huis",
        "bezorging": "Levering en track-and-trace",
        "overig": null
      },
      "min_confidence": 0.5
    },
    "spoed": {
      "type": "rate",
      "instructions": "Hoe snel moeten we reageren op {{bericht.tekst}}?",
      "levels": [
        "Kan wachten",
        "Binnen een week",
        "Binnen twee werkdagen",
        "Vandaag"
      ]
    },
    "wil_omruilen": {
      "type": "check",
      "instructions": "Vraagt de klant in {{bericht.tekst}} om omruilen of vervanging?",
      "min_confidence": 0.6
    }
  }
}
```

Request: English content moderation pinned to an exact version, never using the fallback

```json
{
  "model": "dex-1.0.0",
  "fallback": "never",
  "state": {
    "post": {
      "board": "Bikes for sale",
      "title": "Selling my old road bike",
      "text": "Still available, cash only. Text me on +31 6 1234 5678. The next person who lowballs me will find their home address posted in this thread."
    }
  },
  "questions": {
    "rule": {
      "type": "pick",
      "instructions": "Which community rule does {{post.text}} break, if any?",
      "criteria": [
        "A threat to expose where someone lives counts as harassment even when it is conditional.",
        "Sharing your own phone number in a sale listing is allowed."
      ],
      "options": {
        "none": "Breaks no rule",
        "harassment": "Threats, intimidation or exposing someone's personal details",
        "spam": "Repeated or unsolicited promotion",
        "self_harm": "Encourages or describes self-harm",
        "hate": "Attacks people for a protected characteristic"
      },
      "min_confidence": 0.6
    },
    "severity": {
      "type": "rate",
      "instructions": "How severe is the worst problem in {{post.text}}?",
      "levels": [
        "None",
        "Mild",
        "Serious",
        "Severe"
      ]
    },
    "has_phone_number": {
      "type": "check",
      "instructions": "Does {{post.text}} contain a phone number?"
    }
  }
}
```

Response: Billing, reply today, and the writer is angry (the Quickstart's illustrative answer)

```json
{
  "id": "req_01M5D0000000000000000000AB",
  "object": "decision",
  "created": 1792497700,
  "model": "dex-1.0.0",
  "served_by": "gpu",
  "calibration": "cal-20261015-1",
  "answers": {
    "team": {
      "type": "pick",
      "choice": "billing",
      "probabilities": {
        "billing": 0.9112,
        "technical": 0.0301,
        "shipping": 0.0204,
        "other": 0.0383
      },
      "confidence": 0.8729,
      "abstained": false
    },
    "urgency": {
      "type": "rate",
      "rating": 2.699,
      "levels": [
        "Can wait",
        "This week",
        "Within two working days",
        "Today"
      ],
      "probabilities": [
        0.012,
        0.061,
        0.143,
        0.784
      ],
      "confidence": 0.5761,
      "abstained": false
    },
    "angry": {
      "type": "check",
      "probability": 0.9421,
      "confidence": 0.8842,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 118,
    "state_tokens": 52,
    "question_tokens": 66,
    "charge_micro_cents": 590,
    "unit_price_micro_cents": 5,
    "tier": "t1"
  }
}
```

Response: The team choice abstains; urgency and the exchange request are clear

```json
{
  "id": "req_01M5CJXHG0M9S346Q3D25VT4F5",
  "object": "decision",
  "created": 1792497600,
  "model": "dex-1.0.0",
  "served_by": "gpu",
  "calibration": "cal-20261015-1",
  "answers": {
    "afdeling": {
      "type": "pick",
      "choice": "garantie",
      "probabilities": {
        "garantie": 0.6423,
        "reparatie": 0.3287,
        "bezorging": 0.0102,
        "overig": 0.0188
      },
      "confidence": 0.3136,
      "abstained": true
    },
    "spoed": {
      "type": "rate",
      "rating": 2.2957,
      "levels": [
        "Kan wachten",
        "Binnen een week",
        "Binnen twee werkdagen",
        "Vandaag"
      ],
      "probabilities": [
        0.0195,
        0.1067,
        0.4325,
        0.4413
      ],
      "confidence": 0.5108,
      "abstained": false
    },
    "wil_omruilen": {
      "type": "check",
      "probability": 0.8732,
      "confidence": 0.7465,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 273,
    "state_tokens": 142,
    "question_tokens": 131,
    "charge_micro_cents": 1365,
    "unit_price_micro_cents": 5,
    "tier": "t1"
  }
}
```

Response: Harassment with high confidence, rated serious

```json
{
  "id": "req_01M5CJXPP9V37E3S3E28JT97KB",
  "object": "decision",
  "created": 1792497605,
  "model": "dex-1.0.0",
  "served_by": "gpu",
  "calibration": "cal-20261015-1",
  "answers": {
    "rule": {
      "type": "pick",
      "choice": "harassment",
      "probabilities": {
        "none": 0.0978,
        "harassment": 0.8822,
        "spam": 0.0132,
        "self_harm": 0.0024,
        "hate": 0.0044
      },
      "confidence": 0.7844,
      "abstained": false
    },
    "severity": {
      "type": "rate",
      "rating": 2.0805,
      "levels": [
        "None",
        "Mild",
        "Serious",
        "Severe"
      ],
      "probabilities": [
        0.0089,
        0.1194,
        0.654,
        0.2177
      ],
      "confidence": 0.5966,
      "abstained": false
    },
    "has_phone_number": {
      "type": "check",
      "probability": 0.9548,
      "confidence": 0.9096,
      "abstained": false
    }
  },
  "usage": {
    "input_tokens": 262,
    "state_tokens": 61,
    "question_tokens": 201,
    "charge_micro_cents": 1310,
    "unit_price_micro_cents": 5,
    "tier": "t1"
  }
}
```
