2026-11-13Spend
Classify, Score, and Route Tickets with the Gemini Decisions API
The Gemini Decisions API is a closed-set scoring endpoint, not a chat model. On 11 November 2026 Google introduced it for yes/no checks, fixed-choice routing, and rubric scores over one shared multimodal input. You send text, images, audio, video, or PDFs plus up to 1,000 independent questions. The model, gemini-decision-1, returns probabilities and a confidence score instead of generated answer text. Paid input is listed at $0.10 per million tokens; output tokens are unbilled because the API generates none.
Checked against the live announcement, Decisions API documentation, and Gemini Decision pricing table on 13 November 2026. The 11 November date is Google’s launch note, not this page’s publish date. Vendor latency and “nearly 5x cheaper” figures on the blog are Google’s own claims, not independent timings from this site. This page did not call the API and does not invent a live success payload.
What you get from one request
Each call has two parts: shared input, and a map of questions whose keys you choose. Answers come back under the same keys. Every question is one of three types.
| Type | What you ask | What you read |
|---|---|---|
noul (yes/no) | Whether a statement holds | noul from 0.01 to 0.99, plus confidence |
choice | One option from a closed list | Winning choice, per-option probabilities, confidence |
score | Grade against an ordered rubric | Weighted score, per-level probabilities, legend, confidence |
The docs treat questions as isolated. They cannot see each other. Latency is described as nearly flat when you add more questions. Shared input is billed once per request; each question adds only its own instruction and criteria tokens.
The input field uses the same content-block shape as the Interactions API, minus tool-call, tool-result, and thought blocks. Adjacent text blocks are concatenated with no separator, so you must insert spaces or newlines yourself. Empty input is rejected.
Media is native. You can mix a call recording with account metadata, or a PDF with photos, in one pass. Image and video blocks take an optional resolution of low, medium, high, or ultra_high (ultra-high is images only). Audio and PDF blocks do not take resolution.
When this endpoint fits, and when ordinary generation still wins
Use the Decisions API when the next line of your code is a branch: route a ticket, pick a tool, check a policy, score a document, or decide whether a human should take over. Google’s own table contrasts it with the Interactions API. Decisions is for a yes/no probability, one option from a fixed set, a rating on a fixed scale, many independent checks on the same input, low latency, and zero output-token cost. Interactions is for generated text, summaries, explanations, JSON that follows your own schema, tool calls with arguments, and multi-step reasoning.
Stay on ordinary generation when you need a reply a person will read, a free-form extraction, or an explanation of why the model chose an option. The Decisions API does not return that prose. Confidence is not a written rationale.
Stay on structured output when you need fields that are not a closed set: an order number, a date, a new category invented by the model. Choice questions always distribute probability across the options you listed, even when none fits. That is a feature for routing. It is the wrong primitive for open extraction.
Google’s blog names internal uses (Notebook routing, Stitch Rapid, CC emoji, Search MCP safety). Those are vendor notes, not measurements on your traffic.
Setup, keys, and versions
The endpoint is on the Gemini Developer API, v1beta, with gemini-decision-1 only. The docs require SDK 3.2 or later: pip install -U google-genai or npm install @google/genai@latest. REST goes to https://generativelanguage.googleapis.com/v1beta/decisions with header x-goog-api-key: $GEMINI_API_KEY.
Put the key in the environment. Do not hard-code it. The Python client shown in the docs is genai.Client(http_options={"api_version": "v1beta"}). The JavaScript client is new GoogleGenAI({ apiVersion: "v1beta" }). Both examples below read GEMINI_API_KEY from the environment and stop if it is missing.
Question keys must match [A-Za-z0-9_-]+ and stay within 64 characters. You get 1 to 1,000 questions per request, bounded by a 262,144-token context window across shared input and all questions. Choice options: 2 to 255. Score levels: 2 to 255 non-empty strings, with 2 to 10 recommended. Option keys sort alphabetically in UTF-8 byte order before evaluation; JSON key order does not change positions, and exact probability ties break to the alphabetically first key. Do not use positional catch-alls such as none_of_the_above.
Architecture limits: no YouTube URLs, no gs://, no conversation-history / thought / tool-call input blocks, no context caching, no Batch, no Flex or Priority. Repeated input counts in full. Media: Files API URI, public or signed HTTPS (15 MB per external URL), or inline base64 (100 MB per request, 50 MB for PDFs). Set a real MIME type; octet-stream is rejected.
A reproducible support-ticket example (synthetic, labeled)
The following tickets are synthetic. They are not production traffic and were not sent to Google. Labels are the intended ground truth for a routing prototype: whether the message is a hardware defect, which queue should own it, and how urgent the churn language is. Use them to write criteria, set thresholds on a hold-out set, and log disagreements. Do not treat the labels as API output.
id=T001
order={"id":"8821","item":"over-ear headphones","price_usd":240,"tier":"Gold"}
message=Audio cuts out on the left earcup after 20 minutes. Replace it today or I cancel.
labels: is_defect=yes queue=replacement churn=3_threatens_cancel
id=T002
order={"id":"1044","item":"USB-C cable","price_usd":12,"tier":"Silver"}
message=I ordered blue but the site shipped black. I just want the color I picked.
labels: is_defect=no queue=shipping churn=1_mild
id=T003
order={"id":"3309","item":"laptop stand","price_usd":89,"tier":"Gold"}
message=Invoice 3309 charged me twice this morning. Please reverse the duplicate.
labels: is_defect=no queue=billing churn=1_mild
id=T004
order={"id":"7710","item":"webcam","price_usd":59,"tier":"Bronze"}
message=The app will not detect the camera on Ubuntu 24.04. LED never turns on.
labels: is_defect=maybe_software queue=technical churn=2_mentions_alternatives
T004 is the useful messy row. “LED never turns on” might be hardware. “Will not detect on Ubuntu” might be a driver. A yes/no defect check without criteria will wobble. The docs tell you to add true_description and false_description when the question is ambiguous, and to keep a human or a second check on high-stakes actions.
Map that into three independent questions on the same input, matching the official mix of noul, choice, and score.
is_defect(noul): “Does the customer message describe a hardware defect in the product itself?” Criteria: true = broken or faulty hardware; false = preference, color, sizing, billing, shipping, or software-only setup.queue(choice): billing, technical, shipping, replacement, other. Give each option a one-line description. Keepotheras a catch-all with an explicit description, not a positional key.churn_risk(score): four levels from “no churn signal” through “threatens to cancel,” matching the docs’ Gold-headphones example shape.
Serialize JSON to a string and pass it as text. Refer to fields by key path in the instructions (message, tier, order). Do not dump unrelated CRM history; the docs say unrelated content lowers accuracy. Compare prices and dates in your own code if you need numeric rules, then pass a named bucket.
Python, env-var key only. Reuse the same three questions and T001 payload:
import json, os, sys
from google import genai
if not os.environ.get("GEMINI_API_KEY"):
sys.exit("Set GEMINI_API_KEY in the environment; do not hard-code it.")
ticket = {
"id": "T001",
"order": {"id": "8821", "item": "over-ear headphones", "price_usd": 240, "tier": "Gold"},
"message": "Audio cuts out on the left earcup after 20 minutes. Replace it today or I cancel.",
}
payload = json.dumps({"order": ticket["order"], "tier": ticket["order"]["tier"]})
message = json.dumps({"message": ticket["message"]})
questions = {
"is_defect": {
"type": "noul",
"instructions": "Does `message` describe a hardware defect in the product itself?",
"criteria": {
"true_description": "The product itself is broken or faulty.",
"false_description": "Preference, color, sizing, billing, shipping, or software-only setup.",
},
},
"queue": {
"type": "choice",
"instructions": "Which queue should own this request given `message`, `tier`, and `order`?",
"criteria": {
"billing": "Payments, invoices, duplicate charges, and refunds.",
"technical": "Problems using software or pairing, without clear broken hardware.",
"shipping": "Delivery, tracking, wrong color or size, or missing package.",
"replacement": "Clear hardware fault where a replacement shipment fits.",
"other": "Anything outside these queues.",
},
},
"churn_risk": {
"type": "score",
"instructions": "How strong is the churn language if this is not resolved now?",
"criteria": ["No churn signal", "Mild frustration", "Mentions alternatives", "Threatens to cancel"],
},
}
client = genai.Client(http_options={"api_version": "v1beta"})
result = client.decisions.create(
model="gemini-decision-1",
input=[{"type": "text", "text": payload + "\n"}, {"type": "text", "text": message}],
questions=questions,
)
defect, queue, churn = result.answers["is_defect"], result.answers["queue"], result.answers["churn_risk"]
action = queue.choice if defect.noul > 0.9 and queue.confidence > 0.6 else "human_review"
print(ticket["id"], action, defect.noul, queue.confidence, churn.score)
JavaScript, same questions, env-var key only:
import { GoogleGenAI } from "@google/genai";
const key = process.env.GEMINI_API_KEY;
if (!key) {
console.error("Set GEMINI_API_KEY in the environment; do not hard-code it.");
process.exit(1);
}
const ticket = {
order: { id: "8821", item: "over-ear headphones", price_usd: 240, tier: "Gold" },
message: "Audio cuts out on the left earcup after 20 minutes. Replace it today or I cancel.",
};
const questions = {
is_defect: {
type: "noul",
instructions: "Does `message` describe a hardware defect in the product itself?",
criteria: {
true_description: "The product itself is broken or faulty.",
false_description: "Preference, color, sizing, billing, shipping, or software-only setup.",
},
},
queue: {
type: "choice",
instructions: "Which queue should own this request given `message`, `tier`, and `order`?",
criteria: {
billing: "Payments, invoices, duplicate charges, and refunds.",
technical: "Problems using software or pairing, without clear broken hardware.",
shipping: "Delivery, tracking, wrong color or size, or missing package.",
replacement: "Clear hardware fault where a replacement shipment fits.",
other: "Anything outside these queues.",
},
},
churn_risk: {
type: "score",
instructions: "How strong is the churn language if this is not resolved now?",
criteria: ["No churn signal", "Mild frustration", "Mentions alternatives", "Threatens to cancel"],
},
};
const ai = new GoogleGenAI({ apiVersion: "v1beta", apiKey: key });
const result = await ai.decisions.create({
model: "gemini-decision-1",
input: [
{ type: "text", text: JSON.stringify({ order: ticket.order, tier: ticket.order.tier }) + "\n" },
{ type: "text", text: JSON.stringify({ message: ticket.message }) },
],
questions,
});
const defect = result.answers?.is_defect;
const queue = result.answers?.queue;
if (defect?.type === "noul" && queue?.type === "choice") {
console.log(defect.noul > 0.9 && queue.confidence > 0.6 ? queue.choice : "human_review");
}
This site did not execute those snippets against Google. They are syntax-checked locally with no credentials. For the shape of a mixed-question response, use Google’s published support-ticket sample in the Decisions API docs (headphones, Gold tier, “fix it today or I cancel”). That sample is Google’s illustration, not a measurement from this page. It shows is_defect.noul 0.94, next_action.choice human_escalation at confidence 0.76, churn_risk.score 2.99 with almost all mass on “Threatens to cancel”, total_input_tokens 324, and total_output_tokens 0. Copy the field names from that document. Do not treat the numbers as your production baseline.
How to read output against thresholds and human review
Confidence is documented as the estimated probability, from 0 to 1, that the top answer is right. For yes/no it equals max(noul, 1 - noul). For choice it is the top option’s probability. For score, read the weighted score and the per-level map: different distributions can share the same expected value.
Yes/no probabilities always sit between 0.01 and 0.99. A check of noul > 0.99 never matches. Set thresholds inside that range.
The docs’ routing pattern is the one to copy:
answer = result.answers["queue"]
if answer.confidence >= 0.7:
route_to(answer.choice)
else:
route_to("human_review")
Set 0.7 from your labeled tickets, not from this sentence. Weigh a false auto-refund against a false hold. For T001, a high defect probability plus high replacement confidence can auto-queue a replacement after you have measured that pair on a hold-out set. For T004, low confidence or a split between technical and replacement should go to a person.
Choice questions always spread probability across options. If none of your queues fits, you still get a winner. Pair the choice with a yes/no scope check (“Is this message about an order?”) or require a minimum confidence. Prefer a described other key.
For scores, if you need a discrete level, take max(answer.probabilities, key=answer.probabilities.get) and look up answer.legend. To compare rubrics with different lengths, divide the score by the highest level index.
Dependent decisions (repair category only if damaged) need either a second request after you read the first answer, or both questions in one request with the unused answer ignored. Questions never see each other, so you cannot encode “if yes, then …” inside the question map.
Treat user text as untrusted. The docs say user-supplied text can argue for its own classification. Write explicit criteria, test adversarial wording, and keep a human on payouts.
Cost, privacy, and what is not on the rate card
The Gemini Decision pricing table (anchor gemini-decision) lists Standard only. There is no Batch tab.
| Feature | Free tier | Paid tier, per 1M tokens USD |
|---|---|---|
| Input (text, image, video, audio, PDF) | Free of charge | $0.10 |
| Output | Not applicable (0 output tokens) | Not applicable (0 output tokens) |
| Context caching | Not available | Not available |
| Used to improve our products | Yes (see Google’s API terms) | No (see Google’s API terms) |
Paid output is unbilled because output tokens are always 0. The blog restates $0.10 per 1M input tokens and $0 for output, with an API no-cost tier in Google AI Studio for prototyping. Shared input is billed once no matter how many questions you attach.
The same pricing table says free-tier content is used to improve Google’s products; paid-tier content is not. That is a privacy limit, not a quality claim. Do not put production tickets, IDs, or attachments on the free tier if you cannot accept that use. The Decisions API docs also say input safety filters can return HTTP 400 instead of answers, with error.code values such as safety, prohibited_content, blocklist, image_safety, or content_blocked.
Google’s blog claims 150–250 ms end-to-end, 50–100 ms median model compute up to 16K tokens, and “nearly 5x cheaper” than Gemini 3.5 Flash-Lite on an internal suite. Those are vendor claims. This page did not reproduce them. Do not estimate Decisions cost with a Flash-Lite output-token calculator.
Common errors and input mistakes
HTTP 400 with a named validation rule: too many questions, bad question key, empty input, empty score levels, or option-key whitespace. Fix the field the message names.
HTTP 400 safety block: the JSON error object carries code and message. Modify input; do not retry the same payload in a loop.
MIME mismatch: the API rejects generic octet-stream and content that does not match the block type (image vs audio vs document).
URI scheme: YouTube and gs:// are documented as unsupported. Use Files API URIs, HTTPS, or inline bytes.
SDK too old: client.decisions exists at 3.2+. An older google-genai will fail at import or attribute lookup.
Thresholds outside 0.01–0.99 on noul: the comparison never fires.
Positional option keys: alphabetical sort moves none_of_the_above ahead of shipping.
JSON as nested objects in input instead of serialized text: keys can be misread as content blocks. Serialize first.
PDF as an image block: PDFs use type: "document" and application/pdf.
The docs’ photo, audio, and video snippets use Google demo files (JPEG inline, Apollo MP3, ForBiggerBlazes MP4). They are format examples, not your tickets.
Task routing without turning the classifier into a chatbot
A practical control plane for tickets looks like this:
- Cheap deterministic rules first (duplicate invoice ID, empty body, known bot).
- One Decisions request with atomic questions: in-scope?, defect?, queue?, churn?, needs-photo?
- Code combines answers with
ifstatements. The model never sees your variable names; write the full question ininstructions. - Auto-act only above a threshold you measured. Otherwise human review, and store the probabilities next to the ticket.
- Escalate to a generative model only when a person needs a draft reply.
That last step is where ordinary generation belongs. Decisions can tell you the queue. It will not write the apology.
The same pattern applies to document scoring. Pass a PDF as type: "document" with mime_type: "application/pdf", plus a short score rubric and a noul “is this the signed contract?” check. Keep the rubric linear. Exponential labels (1, 10, 100, 1000) skew the expected value because the score averages array indices.
Labeled synthetic pages for a scoring prototype (again, not API output):
D001 invoice PDF, totals match PO, no handwritten markup complete=yes quality=2_ready
D002 scan of a coffee-stained packing list, two lines unread complete=no quality=0_unusable
D003 signed MSA, last page present, annex missing complete=no quality=1_needs_rescan
Use D001–D003 to pick a noul auto-file threshold and a score cutoff before anything is marked complete. Calibrate on a hold-out you labeled yourself. Google’s printed 0.94 / 0.76 / 2.99 sample is a shape example from the docs, not a threshold you should copy onto invoices.
Calibration is ordinary data work. Hold out at least the four tickets and three documents above, plus a dozen of your own anonymized samples. Record noul, choice, probabilities, score, confidence, and the human label. Plot false auto-routes against confidence. Move the threshold until the expensive error (wrong refund, wrong legal file) is rare enough for your queue. Do not publish a universal cutoff. The docs tell you to set thresholds from labeled examples of your traffic and to weigh false positives against false negatives. If two questions disagree (high defect probability, top queue billing), send the ticket to a person. Questions cannot see each other; only your if statements can reconcile them.
Prefer positive phrasing. Ask “Is the tier Gold?” and subtract noul from 1 in code rather than “Is the tier NOT Gold?” Compare amounts and dates in code, then pass a bucket in the text input. Send only fields the questions need.
Frequently asked questions
What model and API version does the Decisions API use?
gemini-decision-1 on Gemini Developer API v1beta. The docs say it is available with Gemini Decision 1 only.
How many questions can one request hold?
1 to 1,000, bounded by the 262,144-token context window. Mix noul, choice, and score.
What does paid usage cost?
The pricing table lists $0.10 per million input tokens on the paid tier. Output is not applicable because the API generates zero output tokens. Batch, cache, Flex, and Priority are not available for this model.
Does free-tier traffic train the product?
The Gemini Decision pricing table marks “Used to improve our products” as Yes on the free tier and No on the paid tier, linking Google’s API terms. Paid-tier traffic is the documented way to keep that use off.
Can I pass YouTube or Cloud Storage URIs?
No. The limitations list says no YouTube URLs and no direct gs:// URIs. Use a Files API URI, a public or signed HTTPS URL, or inline base64.
Should I use this instead of generating JSON?
Use Decisions when you need calibrated probabilities over a closed set you already defined. Use the Interactions API and structured outputs when you need generated text or objects that follow your own schema.
Sources
Current prices are on the price table.