# Run a decision from the console in test mode

`POST /v1/console/playground/decide` (operation id `playgroundDecide`)

Same request and response shapes as `decide`. Runs in test mode: GPU only, not billed, and counted against
the account's daily test quota of 250,000 tokens. Content is not stored.

Authentication: Console session cookie (`__Host-dex_session`). State-changing routes also need the `x-dex-csrf` header.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `x-dex-csrf` | header | string | yes | The `csrf_token` returned by `createSession`. Required on every state-changing console route. (32 to 64 characters) |

## 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, in test mode.

| 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.

### 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.

### 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.

## Examples

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

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
    }
  }
}
```
