API docs
One decision endpoint in the System One wire format. Call it over plain HTTP, or use typesafe-sdk by changing the base URL.
Endpoint
POST https://staging-j97-api-9tm2.encr.app/v1/systemoneBase URL: https://staging-j97-api-9tm2.encr.app
No key? POST /demo/systemone takes the same body with no auth and no charge, limited to 10 requests per minute per IP and 2,000 input tokens.
Authentication
Authorization: Bearer tia_live_<32 url-safe characters>Send your API key in the Authorization header. Keys look like tia_live_ followed by 32 characters, are created in your account and shown once. Only a SHA-256 hash is stored; if you lose a key, revoke it and create a new one.
Request body
The body is a JSON object:
| field | Required | Description |
|---|---|---|
| model | optional | Leave it out: the API answers with the currently served build. Names starting with "jev" are accepted for typesafe-sdk compatibility. Other values return 400. |
| state | required | Text or any JSON except null. This is what the model reads. |
| questions | required | Object from question name to question definition, 1 to 64 questions. |
{
"state": {
"kenh": "zalo",
"goi_dich_vu": "Pro năm",
"tin_nhan": "Chào shop, tháng này em bị trừ tiền 2 lần cho gói Pro, 499.000đ mỗi lần, hoá đơn thứ hai vẫn đang treo. Nhờ shop hoàn lại khoản bị trừ trùng trong hôm nay giúp em, mai kế toán bên em chốt sổ rồi."
},
"questions": {
"hoan_tien": {
"type": "noul",
"instructions": "Khách có đang yêu cầu trả lại tiền không?",
"criteria": {
"true": "đòi hoàn tiền, huỷ giao dịch hoặc bù tiền",
"false": "không đòi lại tiền"
}
},
"doi_xu_ly": {
"type": "choice",
"instructions": "Đội nào nên nhận ticket này?",
"criteria": {
"thanh_toan": "hoá đơn, trừ tiền, hoàn tiền, gói cước",
"ky_thuat": "lỗi, sự cố, tích hợp",
"tai_khoan": "đăng nhập, quyền truy cập, bảo mật",
"ban_hang": "mua mới, nâng cấp, báo giá"
}
},
"muc_do_gap": {
"type": "score",
"instructions": "Ticket này gấp đến mức nào?",
"criteria": [
"để tuần sau được",
"bình thường, trả lời trong 2 ngày",
"ảnh hưởng tiền hoặc công việc hôm nay",
"sự cố diện rộng, rủi ro pháp lý hoặc bảo mật"
]
}
}
}Question types
| type | criteria | Answer |
|---|---|---|
| noul (or boolean) | Optional: {"true": description, "false": description} | {"type":"noul","noul": P(yes)} |
| choice | Required: {label: description}, 2 to 10 labels | choice (top label), confidence, probabilities per label |
| score | Required: array of descriptions, lowest first, 2 to 10 levels | score (expected level, a float), confidence (top level probability), legend, probabilities per level |
Every question needs instructions: a string stating the question.
Define a decision like a tool call
If you have written a tool for an agent, you already know how to ask TIA. The same three parts: a name, a description, and the options, each with a line of description.
{
"name": "route_ticket",
"description": "Which team should handle this support ticket?",
"parameters": {
"type": "object",
"properties": {
"team": {
"oneOf": [
{ "const": "billing",
"description": "invoices, double charges, refunds" },
{ "const": "technical",
"description": "bugs, crashes, login errors" },
{ "const": "sales",
"description": "pricing questions, upgrades, new contracts" }
]
}
},
"required": ["team"]
}
}{
"state": "I was charged twice for the Pro plan this month, please refund one of the payments.",
"questions": {
"route_ticket": {
"type": "choice",
"instructions": "Which team should handle this support ticket?",
"criteria": {
"billing": "invoices, double charges, refunds",
"technical": "bugs, crashes, login errors",
"sales": "pricing questions, upgrades, new contracts"
}
}
}
}| Agent tool | TIA decision |
|---|---|
| name | the question name, a key in questions |
| description | instructions |
| enum / oneOf values, each with a description | criteria: {label: description}, 2 to 10 labels (required for choice) |
| a true/false parameter | type "noul" (or "boolean"), optional criteria {"true": ..., "false": ...} |
| an ordered scale (say 0 to 3) | type "score", criteria is an array of descriptions, lowest level first |
"route_ticket": {
"type": "choice",
"choice": "billing",
"confidence": 0.915,
"probabilities": {
"billing": 0.915,
"technical": 0.002,
"sales": 0.083
}
}Saved output of POST /demo/systemone for exactly the request above, Sep 30, 2026, req_f81f2e470af3c572eb1d054d. Rounded to 3 digits; not live output from the currently served build.
Here the model chose the wrong label (ky_thuat), with a highest probability of only 0.48. A threshold of 0.8 would escalate the ticket to a person. This illustrates thresholding, not a validated operating threshold.
A structural guarantee
Every answer is a probability distribution over exactly the options you defined. The model cannot return free text or an option you did not define, so the rate of out-of-schema answers is 0%. The guarantee is about the shape of the answer, not about correctness: the top option can still be wrong.
How much the question and the option descriptions matter
Removing either part raises KL (worse) in all four cells, on both sets. Accuracy drops in three cells; the fourth, the neutral question on typed-decisions (-1.3 points, 95% CI [-3.2; +0.5]), is not settled. Which part matters more depends on the task. When option labels carry no meaning by themselves (true/false, 1 to 5), the option descriptions decide it (-20.4 accuracy points on typed-decisions). When labels are self-explanatory, the question text decides it (-13.9 points on the general set). So define every decision like a tool: a clear name and question, and a description for every option.
| Request | typed-decisions dev set (930) | general dev set (8,875) | ||
|---|---|---|---|---|
| Accuracy | KL (lower is better) | Accuracy | KL (lower is better) | |
| Full: the question and a description for every option | 0.759 | 0.134 | 0.878 | 0.173 |
| Option descriptions removed, labels only | -20.4 points [-24.0; -16.8] | +0.246 [+0.217; +0.277] | -2.6 points [-2.9; -2.2] | +0.064 [+0.057; +0.073] |
| The question replaced by a neutral word | -1.3 points [-3.2; +0.5], not settled | +0.029 [+0.021; +0.039] | -13.9 points [-14.8; -13.0] | +0.236 [+0.220; +0.254] |
Rows below the first show the change against the full request, with a 95% confidence interval. The model was built to read the question and the option descriptions, so this measures how much it relies on them, not a rule for every model. Always describe your options.
Source: measured by TIA on Sep 30, 2026, TIA 1.1 mini (the model behind the System One API at the time, before TIA 1.2 mini), internal report on option descriptions; paired bootstrap, 2,000 resamples, 95% confidence intervals. typed-decisions dev set: 930 decisions, the same "dev" set as on the Benchmark page. general dev set: 8,875 decisions, a broader mix of decision tasks, easier than typed-decisions.
Response
A 200 response has this shape (TypeScript types):
type Response = {
id: string; // "req_…", quote it for support
object: "systemone";
model: string;
answers: Record<string, // one entry per question name
| { type: "noul"; noul: number } // P(yes), 0..1
| { type: "choice"; choice: string; confidence: number;
probabilities: Record<string, number> } // sums to 1
| { type: "score"; score: number; confidence: number; // score = expected level
legend: Record<string, string>; // "0" → lowest description
probabilities: Record<string, number> }
>;
cached: boolean; // served from a stored result: cached price
usage: { input_tokens: number; output_tokens: 0;
cached_input_tokens: number; // input_tokens if cached, else 0
free_tokens: number; // free quota used (1/10 per cached token)
cost_micro_vnd: number; cost_display: string };
balance_display: string; // e.g. "12.345 đ"; absent on /demo/systemone
};Errors
Every error looks like {"code": ..., "message": ..., "details": ...}. message is for humans, code is for branching. Model 400s carry details.type = input_too_long or invalid_request.
| HTTP | code | When | What to do |
|---|---|---|---|
| 400 | invalid_argument | Bad body: missing state, wrong type, labels outside 2 to 10, over 64 questions, unknown model (details.type = invalid_request). | Fix the request per message. |
| 400 | invalid_argument | Total input tokens over the limit (details.type = input_too_long). | Shorten the state or split the request. |
| 401 | unauthenticated | Missing, wrong or revoked key. | Check the Authorization header. |
| 402 | payment_required | No free quota left this month and no balance. Checked before calling the model. | Top up and resend. |
| 429 | rate_limited | Over one of your account tier limits (minute, hour, day, concurrent requests) or the demo per-IP limit. The body names which. | Wait the seconds in Retry-After. |
| 503 | unavailable | The model is temporarily unavailable. details.reason = "warming": the model is starting after idle (usually 1 to 4 minutes). | Retry after Retry-After. Not charged. |
Billing and limits
- Every month (Vietnam time) includes 100,000,000 free input tokens, spent first; after that 500 đ per 1M input tokens from your wallet. Output is free. Prices apply from 1 October 2026.
- A repeated request answered from a stored result ("cached": true; same account, same JSON content, same model revision) costs 50 đ per 1M tokens and uses 1/10 of the free tokens; usage.cached_input_tokens counts the tokens billed at that price.
- Input tokens are the state, instructions and criteria descriptions. usage.input_tokens and usage.cost_display in the response belong to that request.
- Request limits apply per account (all its keys share them) and rise with the tier. The tier is your lifetime total of completed top-ups.
- Every response carries an id (req_…). Quote it when asking for support.
| Tier | Lifetime top-up | Requests/min (burst) | Requests/hour | Requests/day | Concurrent | Input tokens/request |
|---|---|---|---|---|---|---|
| Free | 0 đ | 30 (10) | 1,000 | 10,000 | 2 | 4,096 |
| Tier 1 | ≥ 20,000 đ | 300 (60) | 15,000 | 300,000 | 16 | 4,096 |
| Tier 2 | ≥ 500,000 đ | 1,200 (200) | 60,000 | 1,200,000 | 64 | 4,096 |
| Tier 3 | ≥ 5,000,000 đ | 3,000 (500) | 150,000 | 3,000,000 | 128 | 4,096 |
The Free tier is also capped per IP: every free account on one IP shares one daily quota (window ip_day).
Every tier has the same input limit: 4,096 tokens per request, which is the model’s own hard limit. GET /v1/models reports it as max_input_tokens. Over the limit, the response is 400 with details.type input_too_long.
A 429 response carries these headers (use them to pace your calls):
| header | Description |
|---|---|
| X-RateLimit-Limit | Requests per minute allowed on your tier. |
| X-RateLimit-Remaining | Requests left in this minute. |
| X-RateLimit-Reset | Seconds until the minute quota refills. |
| X-RateLimit-Limit-Day | Requests per day allowed (Vietnam time). |
| X-RateLimit-Remaining-Day | Requests left today. |
| Retry-After | Only on 429 and 503: seconds to wait. |
A 429 body names the window that was hit (minute, hour, day, concurrency, ip_day):
{
"code": "rate_limited",
"message": "rate limit exceeded",
"window": "minute",
"limit": 30,
"retry_after_seconds": 2,
"tier": "free",
"upgrade_url": "https://tia.8syncdev.com/pricing"
}Other endpoints
| endpoint | Description |
|---|---|
| GET /v1/models | Served model and limits (max_input_tokens, max_questions, option counts). |
| GET /v1/requests/{id} | Key required. One request of the key’s account; kept 7 days. |
| GET /status | No key. How many model servers are ready, the model and its revision; waking: true while the model is starting after idle (poll again shortly). |
| POST /demo/systemone | No key. Same body, per-IP limit, not charged. |
Examples
The examples read the base URL and key from environment variables; never put a key in code.
export TIA_API_URL="https://staging-j97-api-9tm2.encr.app"
export TIA_API_KEY="tia_live_..."
curl -sS "$TIA_API_URL/v1/systemone" \
-H "Authorization: Bearer $TIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"state":{"kenh":"zalo","goi_dich_vu":"Pro năm","tin_nhan":"Chào shop, tháng này em bị trừ tiền 2 lần cho gói Pro, 499.000đ mỗi lần, hoá đơn thứ hai vẫn đang treo. Nhờ shop hoàn lại khoản bị trừ trùng trong hôm nay giúp em, mai kế toán bên em chốt sổ rồi."},"questions":{"hoan_tien":{"type":"noul","instructions":"Khách có đang yêu cầu trả lại tiền không?","criteria":{"true":"đòi hoàn tiền, huỷ giao dịch hoặc bù tiền","false":"không đòi lại tiền"}},"doi_xu_ly":{"type":"choice","instructions":"Đội nào nên nhận ticket này?","criteria":{"thanh_toan":"hoá đơn, trừ tiền, hoàn tiền, gói cước","ky_thuat":"lỗi, sự cố, tích hợp","tai_khoan":"đăng nhập, quyền truy cập, bảo mật","ban_hang":"mua mới, nâng cấp, báo giá"}},"muc_do_gap":{"type":"score","instructions":"Ticket này gấp đến mức nào?","criteria":["để tuần sau được","bình thường, trả lời trong 2 ngày","ảnh hưởng tiền hoặc công việc hôm nay","sự cố diện rộng, rủi ro pháp lý hoặc bảo mật"]}}}'import os, time
import requests # pip install requests
BASE = os.environ["TIA_API_URL"]
KEY = os.environ["TIA_API_KEY"]
body = {
"state": "Em bị trừ tiền 2 lần cho gói Pro tháng này, nhờ shop hoàn lại giúp em.",
"questions": {
"hoan_tien": {"type": "noul", "instructions": "Khách có yêu cầu trả lại tiền không?"},
"muc_do_gap": {
"type": "score",
"instructions": "Ticket này gấp đến mức nào?",
"criteria": ["để tuần sau được", "bình thường", "ảnh hưởng tiền hôm nay"],
},
},
}
for attempt in range(3):
r = requests.post(f"{BASE}/v1/systemone", json=body, timeout=30,
headers={"Authorization": f"Bearer {KEY}"})
if r.status_code not in (429, 503):
break
time.sleep(float(r.headers.get("Retry-After", "1")))
if r.status_code != 200:
err = r.json()
raise SystemExit(f"{r.status_code} {err['code']}: {err['message']}")
data = r.json()
print(data["answers"]["hoan_tien"]["noul"]) # P(có)
print(data["answers"]["muc_do_gap"]["score"]) # mức kỳ vọng, thấp nhất = 0
print(data["usage"]["cost_display"], data["balance_display"])const BASE = process.env.TIA_API_URL;
const KEY = process.env.TIA_API_KEY;
const res = await fetch(`${BASE}/v1/systemone`, {
method: "POST",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
state: { tieu_de: "Không thanh toán được bằng VietQR", nen_tang: "Android 3.4.1" },
questions: {
thanh_phan: {
type: "choice",
instructions: "Lỗi nằm ở thành phần nào?",
criteria: { app_mobile: "ứng dụng Android/iOS", cong_thanh_toan: "cổng thanh toán, VietQR" },
},
},
}),
});
const data = await res.json();
if (!res.ok) {
// 400 invalid_argument, 401 unauthenticated, 402 payment_required, 429 / 503 (see Retry-After)
throw new Error(`${res.status} ${data.code}: ${data.message}`);
}
console.log(data.answers.thanh_phan.choice, data.answers.thanh_phan.probabilities);// npm install @typesafe-ai/sdk
// .env
// TIA_API_URL=https://staging-j97-api-9tm2.encr.app
// TIA_API_KEY=tia_live_...
import { TypeSafeClient, choice, noul, score } from "@typesafe-ai/sdk";
process.env.TYPESAFE_BASE_URL = process.env.TIA_API_URL;
process.env.TYPESAFE_API_KEY = process.env.TIA_API_KEY;
const client = new TypeSafeClient();
const { answers } = await client.systemOne({
state: { tin_nhan: "Em bị trừ tiền 2 lần, nhờ shop hoàn lại giúp em." },
questions: {
hoan_tien: noul("Khách có yêu cầu trả lại tiền không?"),
doi_xu_ly: choice("Đội nào nên nhận ticket?", {
thanh_toan: "hoá đơn, trừ tiền, hoàn tiền",
ky_thuat: "lỗi, sự cố",
}),
muc_do_gap: score("Gấp đến mức nào?", ["để tuần sau", "bình thường", "trong hôm nay"]),
},
});
answers.doi_xu_ly.choice; // "thanh_toan" | "ky_thuat"typesafe-sdk
The example below reads TIA_API_URL and TIA_API_KEY, then passes them through the TYPESAFE_BASE_URL and TYPESAFE_API_KEY configuration variables used by @typesafe-ai/sdk. It sets no model, so the API answers with the currently served build, not experimental candidate TIA 1.5.
TYPESAFE_BASE_URL=https://staging-j97-api-9tm2.encr.app
TYPESAFE_API_KEY=tia_live_...