Introduction
A decision API that answers in one word. It's live, not just a demo.
Some questions about text need a typed answer, not a paragraph: is this email spam, which team should handle this ticket, how urgent is it. Decision APIs such as TypeSafe's Jev return exactly that: a true or false, one of your options, or a level on your scale.
Haiku Decision API is a working prototype of that idea built on Claude Haiku 5.5, with nothing added but configuration. It takes Jev's request shape, asks Haiku each question with effort set to low and thinking turned off, and accepts the reply only if it exactly matches one of your labels. Anything else comes back as an error for that question, never as a guess.
It is free to call, with no key, within a small daily budget. It returns decisions only, without probabilities. The code is open source under the Apache License 2.0 on GitHub.
Request
POST /haiku-decision-api/v1/systemone
{
"model": "haiku-decision-2026-10-22",
"state": {"subject": "Invoice overdue", "body": "Hi, your invoice #4411 is three weeks late..."},
"questions": {
"is_spam": {"type": "noul", "instructions": "Is this email spam?"},
"team": {"type": "choice", "instructions": "Which team should handle it?",
"criteria": {"Billing": "Payments, invoices, refunds", "Technical": null}}
}
}
Response
{
"model": "haiku-decision-2026-10-22",
"answers": {
"is_spam": {"type": "noul", "decision": false},
"team": {"type": "choice", "choice": "Billing"}
},
"usage": {"input_tokens": 212, "output_tokens": 2},
"latency_ms": 734,
"no_probability_notice": "This API returns decisions only. …"
}
The response figures in this example are illustrative.
Performance & Cost
What turning thinking off buys, and what it costs.
We sent the same 1,000 emails from the SpamAssassin public corpus (half spam, half not spam) through this API twice: once with its own settings, once with Haiku's default settings. Same prompt, same question ("Is this email spam?"), one request at a time, alternating which configuration went first.
| Haiku Decision | Haiku 5.5, default settings | |
|---|---|---|
| Median latency | 0.663 s | 0.819 s |
| 95th percentile | 1.426 s | 2.049 s |
| Mean | 0.775 s | 1.006 s |
| Accuracy | 96.5% | 98.2% |
| Spam caught | 98.4% | 98.2% |
| Not-spam kept | 94.6% | 98.2% |
| Hard not-spam kept | 67.9% | 89.3% |
| No answer | 0.1% (1) | 0.0% (0) |
| Input tokens per call (mean) | 608.7 | 609.7 |
| Output tokens per call (mean) | 3.0 | 43.3 |
| Cost per call | $0.0000624 | $0.0000826 |
| Cost of all 1,000 calls | $0.0624 | $0.0826 |
- Speed: the median call is 1.23× faster (0.663 s against 0.819 s).
- Quality is the cost of speed, not a score to beat. With thinking off, Haiku decides on its first read. It lost the most on the corpus's "hard ham": legitimate mail that looks like marketing. 9 of those 28 went to SPAM, against 3 with default settings.
-
No answer: 1 of Haiku Decision's 1,000 replies was
neither
truenorfalse: Haiku began describing the email instead and ran out of its 16 tokens. The API returned aninvalid_outputerror for it, never a guess, and it counts as not correct. Default settings: 0. In three later tries on that email, lite answered wrongly twice and described it once. - Cost: prices are Anthropic's published Haiku 5.5 rates, $0.10 per million input tokens and $0.50 per million output tokens (retrieved 2026-10-22). Both configurations read the same input; the default settings also write thinking on many calls.
Measured on 2026-10-22 from Chicago over Wi-Fi. Each request went to this API's own code running on the same Mac, so the times include about 3 ms of API overhead but not the trip to our server in Iowa. Times are wall clock around each request on the client, one request at a time, after one throwaway warm-up call per setting; no retries in any figure. An earlier run the same day, calling Haiku directly without this API, gave medians of 0.751 s and 0.914 s (1.22×). Your latency depends on where you are.
Through the public endpoint
We also sent the same 1,000 emails to the public address,
nguyennhattran.com/haiku-decision-api/v1/systemone, with this API's settings only (Haiku's
default settings are not served publicly). Measured on 2026-10-22 from Chicago over Wi-Fi, one
request at a time, after one warm-up request.
| Median, as the caller waited | 0.721 s |
|---|---|
| 95th percentile, as the caller waited | 1.774 s |
| Mean, as the caller waited | 0.912 s |
Median latency_ms reported by the API | 0.652 s |
| Accuracy | 96.8% |
| No answer | 0.0% (0) |
The gap between the caller's median and the API's own latency_ms (69 ms) is mostly the
network: from Chicago through Google's front end and our gateway to the server in Iowa, and back.
Quality by corpus folder
| Folder | Haiku Decision | Haiku 5.5, default settings |
|---|---|---|
easy_ham | 94.7% | 98.0% |
easy_ham_2 | 98.8% | 100.0% |
hard_ham | 67.9% | 89.3% |
spam | 98.7% | 98.7% |
spam_2 | 98.3% | 98.0% |
Live runs, last 30 days
From the Run it live button, shown apart from the benchmark. Loading this tally wakes the API.
What others say about speed
Vendor claims, not measured by this project:
- OpenAI states its Decisions API returns typed answers "about 10x faster than the Responses API" (OpenAI Decisions guide, retrieved 2026-10-22).
- TypeSafe states Jev responds in "70ms-500ms" end to end (TypeSafe launch post, retrieved 2026-10-22).
These are different kinds of statements from ours (a ratio with no baseline figures, and a range with no percentile or location), so they are not a like-for-like comparison.
Limitations
- Unofficial. A personal prototype built on the public Claude API. Not an Anthropic product, not endorsed by Anthropic or TypeSafe.
- No probabilities. Responses carry decisions only, because none have been measured or calibrated here. Why.
- Quality has a cost. Thinking is off, so hard cases fare worse (see Performance & Cost). Check it against your own data before relying on it.
- Thinking is off, not just turned down. Anthropic's guidance names effort as the main lever for trading quality against speed. We lower effort and also turn thinking off. With thinking on, Haiku may still write a reasoning step before it answers, even at low effort. With it off, every call answers straight away. That fits what this API is for: a quick gut check on a piece of text, the same job Jev does, not long multi-step reasoning. (We benchmarked these settings against Haiku's defaults, not against low effort with thinking on.)
- The benchmark corpus is old and public. The SpamAssassin corpus dates from 2002 to 2005 and is very likely in the model's training data. Treat the accuracy figures as a sanity check, not a forecast.
- Latency depends on location. Our figures are from Chicago. Every request also passes through a gateway and a server in Iowa (us-central1).
-
Small daily budget, cold starts. The API is free within $0.50 of model spend per day (UTC).
After that it returns 429 until midnight UTC. It sleeps after about 15 minutes without requests, and waking
takes 5 to 50 seconds (Cold starts). Before a batch, send
GET /healthand wait for its answer. - Text only. No images, no
assets, noboxquestions. - Data. Request content is never stored by this API. Requests are sent to Anthropic's API and handled under Anthropic's standard API data policy. Google Cloud's request log records each request's IP address and path.
- Corpus licence. The SpamAssassin public corpus carries no licence grant. Copyright in the messages stays with their original senders. The demo shows short excerpts for demonstration only, with addresses masked. Emails whose excerpt contains explicit words are never displayed, in the waterfall, the replay or live runs; they are still in every count and figure (8 of the replay's 100 sampled emails).
Getting Started
No key, no signup. Send a POST with JSON.
To run your own copy, the full source is on GitHub.
Cold starts: the API sleeps when idle and takes up to about a minute to wake. Call GET /health
first and wait for its answer.
curl
curl https://nguyennhattran.com/haiku-decision-api/v1/systemone \
-H "Content-Type: application/json" \
-d '{
"model": "haiku-decision-2026-10-22",
"state": "Your order #1182 has shipped and arrives Friday.",
"questions": {
"is_spam": {"type": "noul", "instructions": "Is this message spam?"},
"urgency": {"type": "score", "instructions": "How urgent is it?",
"criteria": ["Can wait", "This week", "Today"]}
}
}'
Python (standard library only)
import json
import urllib.request
body = {
"model": "haiku-decision-2026-10-22",
"state": {"subject": "Charged twice", "body": "My card was billed twice for March."},
"questions": {
"team": {"type": "choice", "instructions": "Which team should handle it?",
"criteria": {"Billing": "Payments and refunds", "Technical": None}},
},
}
request = urllib.request.Request(
"https://nguyennhattran.com/haiku-decision-api/v1/systemone",
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request) as response:
answer = json.load(response)["answers"]["team"]
print(answer.get("choice") or answer["error"])
JavaScript
const response = await fetch("https://nguyennhattran.com/haiku-decision-api/v1/systemone", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "haiku-decision-2026-10-22",
state: "Congratulations! You have won a free cruise. Click to claim.",
questions: { is_spam: { type: "noul", instructions: "Is this message spam?" } },
}),
});
const { answers } = await response.json();
console.log(answers.is_spam.decision ?? answers.is_spam.error);
Always check each answer for error: a question can fail while the others succeed.
Docs
Everything you need to call the API, on this page.
Endpoint
POST https://nguyennhattran.com/haiku-decision-api/v1/systemone
Content-Type: application/json
No trailing slash. JSON in, JSON out. No authentication. Any origin may call it from a browser (CORS open).
Request
| Field | Type | Notes |
|---|---|---|
model | string, required | Must be haiku-decision-2026-10-22. |
state | string, object or array, required | The content every question is about. Objects and arrays are sent to the model as JSON. Up to 16,000 characters (objects and arrays measured as compact JSON). |
questions | object, required | 1 to 10 questions, keyed by IDs you choose (up to 64 characters). IDs are never shown to the model; answers come back under the same IDs. |
Each question:
| Field | Type | Notes |
|---|---|---|
type | "noul", "choice" or "score" | |
instructions | string, object, array or null | The question. Optional for choice and score; a noul needs instructions or criteria. |
criteria | depends on type | See below. instructions plus criteria up to 4,000 characters per question. |
Question types
| Type | criteria | Answer |
|---|---|---|
noul (yes or no) | Optional object with keys true and/or false, each a description of when that answer applies. | {"type": "noul", "decision": true} |
choice (pick one) | Object of 1 to 255 options: option name → description, or null for no description. Order is kept. | {"type": "choice", "choice": "<option name>"} |
score (a level on a scale) | Array of 1 to 10 level descriptions, lowest first. | {"type": "score", "level": 1, "legend": {"0": "…", "1": "…", "2": "…"}}. Levels count from 0. |
Response
| Field | Notes |
|---|---|
model | haiku-decision-2026-10-22 |
answers | One entry per question ID, in your order. Each has type and either its answer field or error. |
usage | input_tokens and output_tokens, summed over all questions. |
latency_ms | Time this API spent waiting on the model: from the first call starting to the last one returning. Questions run in parallel. Network time to and from you is not included. |
no_probability_notice | A one-sentence notice with a link to Why there are no probabilities. |
There are no probability, confidence or expected-value fields: they are absent, not null.
How answers are checked
Each question is its own call to Haiku, which must reply with a single label: true or
false, an option number, or a level number (the API numbers your options and levels and maps
the reply back). Surrounding whitespace is trimmed; anything else, including a different case, a trailing
period or an extra word, is rejected. A rejected reply becomes an invalid_output error for that
question, never a guess. This also keeps the endpoint from working as a general-purpose chat proxy.
Errors in an answer (HTTP 200)
error.code | Meaning |
|---|---|
invalid_output | The model's reply did not match one of your labels exactly. |
refusal | The model declined to answer. |
upstream_error | The model call failed or took longer than 10 s. There are no automatic retries; try again. |
"team": {"type": "choice", "error": {"code": "invalid_output", "message": "Model output did not match an option number."}}
Errors for the whole request
| HTTP | Body | When |
|---|---|---|
| 413 | {"error": {"code": "body_too_large", "message": "…"}} | The body is over 256 KiB. |
| 422 | {"detail": [{"loc": […], "msg": "…", "type": "…"}]} | A field is missing, has the wrong type or breaks a limit. loc names the field. Nothing is ever trimmed silently. |
| 429 | {"error": {"code": "rate_limited", "message": "…", "retry_after_s": 12}} | More than 100 requests in a minute from your address. Retry-After header in seconds. |
| 429 | {"error": {"code": "budget_exhausted", "message": "…", "reset_at": "2026-10-23T00:00:00Z"}} | Today's budget is used up. Retry-After header until the reset. |
| 503 | {"error": {"code": "unavailable", "message": "…"}} | The API cannot reach its database, so it cannot track the budget. Try again later. |
Limits
| Limit | Value |
|---|---|
| Request body | 256 KiB |
state | 16,000 characters |
| Questions per request | 10 |
instructions + criteria, per question | 4,000 characters |
| Choice options | 1 to 255 |
| Score levels | 1 to 10 |
| Question ID | 64 characters |
| Model call timeout | 10 s per question |
| Images | not supported |
Rate limits and pricing
- Free to call. No key, no account.
- Daily budget: $0.50 of model spend per UTC day for the public API (about 8,000 one-question calls at the benchmark's average email size), counted from Anthropic's published prices and each call's returned usage. When it runs out, every request returns 429
budget_exhausteduntil 00:00 UTC. The demo's live runs have their own separate $0.50 budget. - Per address: 100 requests per minute. Bursts are fine; the budget is the hard limit.
- Live runs (the demo button): 3 per minute and 20 per hour per address, one at a time.
Cold starts
The API runs on Google Cloud Run and scales to zero: about 15 minutes after the last request it shuts down,
so it costs nothing while idle. The next request starts it again and waits until it's ready. In our
measurements that took 5 to 35 seconds after a short quiet spell and up to about 50 seconds after hours of
quiet (Cloud Run's logs, 2026-10-22 to 2026-10-23). Google doesn't guarantee these times.
latency_ms never includes the wait, and neither does any figure on this page.
Only one instance runs at a time. Requests that arrive while it starts may be refused by Cloud Run itself
with 429, 500 or 503 and a plain-text body. This API's own errors always come as JSON with
error.code. Before a batch, send GET /health (no model call, nothing spent) with a
timeout of at least 90 seconds, wait for its answer, then send the rest. Don't retry in parallel while it
wakes.
Model and settings
Every request is one call per question to Anthropic's Messages API with exactly these settings:
| Setting | Value |
|---|---|
| Model | claude-haiku-5-5 |
| Effort | low (output_config.effort) |
| Thinking | disabled |
max_tokens | 16 |
temperature, top_p, top_k | not sent (Anthropic's defaults) |
| Retries | none |
| Timeout | 10 s |
The model name haiku-decision-2026-10-22 stands for exactly this configuration and will not
change meaning. The benchmark's comparison configuration, "Haiku 5.5, default settings", is effort
medium with adaptive thinking and max_tokens 4096; it is used only in the demo, not
served by the API.
Why there are no probabilities
This API returns decisions only. Probabilities are omitted because none have been measured or calibrated.
Jev's answers carry probabilities: a noul's noul field is the probability of yes, a choice has
confidence and probabilities, and a score's score is an expected value
across levels. This prototype leaves all of those out entirely rather than sending null or a
placeholder, so a client that relies on them fails visibly instead of silently comparing against a made-up
number. In their place, a noul answers with decision (true or false) and a score with
level (one whole level). choice and legend keep Jev's meaning.
Differences from Jev
- No API key; a daily budget and per-address rate limits instead.
- No probability fields (above);
decisionandlevelreplace Jev'snoulandscore. - No images:
assetsandboxquestions are rejected. - One model name,
haiku-decision-2026-10-22, and lower size limits (see Limits).
Source and licence
The code is open source under the Apache License 2.0: github.com/NguyenTran2002/haiku-decision-api. Email excerpts shown in the demo come from the SpamAssassin public corpus and are not covered by that licence.
Data and privacy
- This API never stores or logs request content, question IDs or your IP address. It keeps daily counts and spend only, deleted after 30 days.
- Your
stateand questions are sent to Anthropic's API and handled under Anthropic's standard API data policy. - Google Cloud's request log records each request's IP address and path.