# Triage support tickets in one request with OpenAI's Decisions API

> OpenAI's Decisions API returns a probability, a choice, or a score. Triage a support ticket with several questions in one request.

Author: [Flavio Copes](https://flaviocopes.com/about/) | Published: 2026-10-23 | Topics: [AI](https://flaviocopes.com/tags/ai/) | Canonical: https://flaviocopes.com/openai-decisions-api/

Yesterday, October 22, OpenAI made the **Decisions API** generally available.

It's a dedicated endpoint for small questions with bounded answers. Is this a duplicate charge? Which department gets this ticket? How severe is this bug? You don't get free text back. You get a probability, a choice from your list, or a score.

OpenAI says it answers about 10x faster than running the same model, `gpt-6-luna`, through the Responses API. It's also cheap. Input costs $0.10 per million tokens, cached input costs $0.01 per million, and output tokens aren't charged at all.

Apps and agents make lots of these small decisions: routing, classifying, filtering. Until now we used a full chat completion for each one and parsed the result. This endpoint does just that job.

## The three question types

Every request has a `model`, an `input` (the evidence) and a `questions` array. Each question has a `type`:

- `predicate` checks a condition and returns a `probability` from 0 to 1
- `choice` picks one value from the options you give it
- `score` rates the input against 2 to 10 ordered `levels`

The only supported model right now is `gpt-6-luna`.

```mermaid
flowchart TD
  accTitle: One request, three question types
  accDescr: A support ticket goes to the Decisions API. The same request returns a predicate probability, a department choice, and a severity score.
  A[Support ticket] --> B["POST /v1/decisions"]
  B --> C[Predicate probability]
  B --> D[Department choice]
  B --> E[Severity score]
```

## Your first decision with curl

Let's route a complaint to a department. Send a `choice` question to `POST /v1/decisions`:

```bash
curl https://api.openai.com/v1/decisions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-6-luna",
    "input": "I was charged twice for my order.",
    "questions": [{
      "type": "choice",
      "name": "department",
      "instructions": "Which department should handle this complaint?",
      "choices": [
        {"value": "billing", "description": "Payments, invoices, and refunds."},
        {"value": "technical", "description": "Problems using the product."},
        {"value": "shipping", "description": "Delivery and tracking."},
        {"value": "other", "description": "Requests outside these categories."}
      ]
    }]
  }'
```

You get back an `answers` array. Each answer carries the `name` you gave the question, so you can match them up:

```json
{
  "answers": [
    {
      "type": "choice",
      "name": "department",
      "choice": "billing",
      "probabilities": [
        { "value": "billing", "probability": 0.95 },
        { "value": "technical", "probability": 0.02 },
        { "value": "shipping", "probability": 0.01 },
        { "value": "other", "probability": 0.02 }
      ],
      "confidence": 0.93
    }
  ]
}
```

That response is the example from OpenAI's docs, so your numbers will differ.

Notice the `other` option. OpenAI recommends a fallback like this when your categories don't cover every input, so you can send those tickets to a general queue.

## Ask several questions at once

One request can hold up to 400 questions, and each one can be a different type. Let's build a small triage script in Node.js that asks three things about the same ticket.

Install the official SDK first:

```bash
npm install openai
```

The client reads `OPENAI_API_KEY` from your environment. Save this as `triage.mjs`:

```javascript
import OpenAI from 'openai'

const client = new OpenAI()

const decision = await client.decisions.create({
  model: 'gpt-6-luna',
  input: 'Export fails in Safari but works in Chrome. I need the CSV for my invoice today.',
  questions: [
    {
      type: 'predicate',
      name: 'urgent',
      instructions: 'Does the customer say they need this solved today?'
    },
    {
      type: 'choice',
      name: 'department',
      instructions: 'Which department should handle this ticket?',
      choices: [
        { value: 'billing', description: 'Payments, invoices, and refunds.' },
        { value: 'technical', description: 'Problems using the product.' },
        { value: 'other', description: 'Requests outside these categories.' }
      ]
    },
    {
      type: 'score',
      name: 'severity',
      instructions: 'How severe is this issue?',
      levels: [
        { label: 'Cosmetic', description: 'Appearance only; no lost functionality.' },
        { label: 'Workaround available', description: 'A task fails, but another way works.' },
        { label: 'Fully blocked', description: 'A task fails with no workaround.' }
      ]
    }
  ]
})

for (const answer of decision.answers) {
  if (answer.type === 'refusal') console.log(`${answer.name}: refused`)
  if (answer.type === 'predicate') console.log(`${answer.name}: ${answer.probability}`)
  if (answer.type === 'choice') console.log(`${answer.name}: ${answer.choice}`)
  if (answer.type === 'score') console.log(`${answer.name}: ${answer.score}`)
}
```

Run it:

```bash
node triage.mjs
```

Be careful with the `refusal` type. Any answer can come back as a refusal, so check for it before reading the other fields.

The `score` can fall between levels. Level indices start at 0, and the score is the probability-weighted average of those indices. In OpenAI's example for this same Safari bug, probabilities of 0.1, 0.7 and 0.2 give a score of 1.1, just above "Workaround available".

## Pass JSON directly

New in the GA release, you can pass structured data with an `input_json` content part and skip turning it into a string. It only works in a `user` message:

```javascript
const decision = await client.decisions.create({
  model: 'gpt-6-luna',
  input: [
    {
      role: 'user',
      content: [
        { type: 'input_text', text: 'Evaluate the ticket using the order evidence.' },
        {
          type: 'input_json',
          data: {
            ticket: { body: 'Please refund the duplicate charge.' },
            order: { charges: [49, 49] }
          }
        }
      ]
    }
  ],
  questions: [
    {
      type: 'predicate',
      name: 'duplicate_charge',
      instructions: 'Does the ticket describe a duplicate charge supported by the order?'
    }
  ]
})

const answer = decision.answers[0]

if (answer.type === 'predicate') {
  console.log(answer.probability)
} else {
  console.log('Could not evaluate the charge. Send it for review.')
}
```

Keys like `role` inside `data` are treated as plain data, so they won't create new messages.

You can also send images. Add an `input_image` part with a base64 data URL or a public HTTPS URL. `file_id` inputs aren't supported.

## Pick your thresholds

The API gives you probabilities. Deciding what to do with them is your job. My advice is to run 50 or so real tickets you've already labeled, look at the numbers, and choose a cutoff based on what a mistake costs you. A missed urgent ticket probably costs more than a false alarm.

If you send the same long context on every call (a policy document, for example), keep it identical at the start of `input`. Decisions caches a shared prefix of at least 1,024 tokens by default. Questions sit outside that prefix, so you can change them freely and still get the cached price.

You can also try questions in the Decisions Playground before writing any code.

## Sources

- [OpenAI announcement: Decisions API](https://openai.com/index/decisions-api/)
- [Decisions developer guide](https://developers.openai.com/api/docs/guides/decisions)
- [OpenAI API changelog](https://developers.openai.com/api/docs/changelog)
