Jev is installed three different ways, and the fastest way to find out whether it fits your product is to make one real call rather than read about it. This page gives you the install commands, complete request payloads for all three question types, the response shape you get back, three recipes that end in working decisions, and the error bodies you will actually see.
Everything below was checked against TypeSafe's own documentation on October 12, 2026, and every error body was captured by calling the live endpoint that day.
Install the SDK, export your key, and run the starter. This is the whole loop: state in, typed judgment out.
pip install typesafe-sdk export TYPESAFE_API_KEY="your-key-from-the-typesafe-console"
from typesafe_sdk import Noul, TypeSafeClient ticket = "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP." with TypeSafeClient() as client: response = client.system_one( model="jev-latest", state=ticket, questions={ "is_urgent": Noul( instructions="Does this message express urgency?", ), }, ) print(response.answers["is_urgent"].noul) # 0.99, not "yes"
That printed value is the design difference in one line. You are not parsing a sentence to find an answer. You get a number your code can threshold on, and the documented Noul example returns `0.99` for a message asking to talk to a person and `0.93` for a message that mentions contacting support before.
The same call in curl, if you would rather not install anything yet:
curl -X POST https://api.typesafe.ai/v1/systemone \ -H "Authorization: Bearer $TYPESAFE_API_KEY" \ -H "Content-Type: application/json" \ -d @- <<'EOF' { "state": "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.", "model": "jev-latest", "questions": { "urgency": { "type": "noul", "instructions": "Does this message express urgency?" } } } EOF
The skill and the SDK are different tools for different jobs rather than two ways to do the same thing. The skill changes how your agent designs and writes code. The SDK is what your code calls at runtime. Installing the skill does not remove the need for an API key.
Two commands, both from the official skill repository:
claude plugin marketplace add typesafe-ai/skills claude plugin install typesafe@typesafe-ai
Note the difference in arguments. The marketplace is added by repository path and the plugin installs as `typesafe@typesafe-ai`. Typing the repository path in the second command is a common mistake, and the error message does not explain it.
Once installed, invoke the skill with `/typesafe:typesafe-ai`. Updates come through the plugin system: open `/plugin`, go to Marketplaces, choose `typesafe-ai`, and enable auto-update.
Codex and other skill-compatible agents take the second route:
npx skills add typesafe-ai/skills --skill typesafe-ai
The installer asks which agent you want, which is why one command works across hosts. Two details matter. Installation is project-local by default, so a fresh checkout does not have it; add `-g` to install globally. And updates are manual: run `npx skills update`, or replace the skill directory with the current version. TypeSafe's installation guide states the rule directly: use one installation method.
The Python client installs as `typesafe-sdk` and ships synchronous and asynchronous interfaces. The JavaScript and TypeScript client installs as `@typesafe-ai/sdk`, needs Node 20 or newer, and infers answer types from the questions you write:
npm install @typesafe-ai/sdk
import { choice, TypeSafeClient } from "@typesafe-ai/sdk"; const client = new TypeSafeClient(); const response = await client.systemOne({ state: { document: "I was charged twice. Please fix this ASAP." }, questions: { category: choice("What is this ticket about?", { billing: null, technical: null, other: null, }), }, }); console.log(response.answers.category.choice);
Under both SDKs sits one documented endpoint, `POST https://api.typesafe.ai/v1/systemone`, with `Authorization: Bearer $TYPESAFE_API_KEY` and a JSON body of `state`, `model`, and `questions`. The key is a server-side credential. Do not put it in a browser bundle.
Every question is an object with a `type` and `instructions`. Choice adds `criteria` as an object of named options. Score adds `criteria` as an ordered list. Noul can be asked bare, or with explicit `true` and `false` criteria when the default reading of the question is not what you mean.
curl -X POST https://api.typesafe.ai/v1/systemone \ -H "Authorization: Bearer $TYPESAFE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "state": "Help! My payouts have been failing for 3 days.", "model": "jev-latest", "questions": { "is_urgent": { "type": "noul", "instructions": "Does this convey urgency?", "criteria": { "true": "Explicitly time-sensitive", "false": "No urgency expressed" } } } }'
{ "model": "jev-1.13.0", "answers": { "is_urgent": { "type": "noul", "noul": 1.0 } }, "usage": { "input_tokens": 392, "output_tokens": 65 } }
A Noul answer carries no `confidence` field. The number itself is the answer, so your threshold is the entire policy: above 0.9 treat it as urgent, below 0.8 treat it as not, and send the band in between to a person.
curl -X POST https://api.typesafe.ai/v1/systemone \ -H "Authorization: Bearer $TYPESAFE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "state": "My running shoes arrived in the wrong size. Can I swap them for a size 10?", "model": "jev-latest", "questions": { "department": { "type": "choice", "instructions": "Which team should handle this?", "criteria": { "returns": "Exchanges, wrong or damaged items", "shipping": "Delivery status, delays, lost packages", "billing": "Charges, invoices, payment problems" } } } }'
{ "model": "jev-1.13.0", "answers": { "department": { "type": "choice", "choice": "returns", "confidence": 0.78, "probabilities": { "returns": 0.85, "shipping": 0.0, "billing": 0.15 } } }, "usage": { "input_tokens": 332, "output_tokens": 18 } }
Read both numbers. `choice` is the selected option and `probabilities` tells you how close the runner-up was. The choice reference describes `confidence` as separate from the per-option probability, which is why the example above can show `confidence` 0.78 while `returns` sits at 0.85: confidence is a property of the decision, not a copy of the winning probability.
curl -X POST https://api.typesafe.ai/v1/systemone \ -H "Authorization: Bearer $TYPESAFE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "state": "The export button crashes the settings page in Safari. It works in Chrome, but a few of our customers only use Safari.", "model": "jev-latest", "questions": { "bug_severity": { "type": "score", "instructions": "How severe is the reported issue?", "criteria": [ "Cosmetic; no impact to functionality", "Broken or degraded feature, but workaround exists", "Blocking issue; no workaround exists" ] } } }'
{ "model": "jev-1.13.0", "answers": { "bug_severity": { "type": "score", "score": 1.43, "confidence": 0.35, "legend": { "0": "Cosmetic; no impact to functionality", "1": "Broken or degraded feature, but workaround exists", "2": "Blocking issue; no workaround exists" }, "probabilities": { "0": 0.0, "1": 0.57, "2": 0.43 } } }, "usage": { "input_tokens": 332, "output_tokens": 18 } }
A score is not an integer. `1.43` with `confidence` 0.35 is the model telling you it sits between the levels and is unsure, which is a useful signal. The `legend` field echoes your rubric back so a stored response is readable months later without the prompt that produced it, and the score reference documents both fields.
Questions are independent keys in one object, so one request can answer several at once. This is the shape the quickstart uses:
{ "state": "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.", "model": "jev-latest", "questions": { "department": { "type": "choice", "instructions": "Which team should handle this", "criteria": { "billing": "Payment or subscription issues", "technical": "Bugs or integration problems", "sales": "Pricing or account questions" } }, "frustration": { "type": "score", "instructions": "How frustrated the customer appears", "criteria": [ "Calm, just stating facts", "Frustrated but civil", "Very angry, strong language" ] }, "is_urgent": { "type": "noul", "instructions": "The message conveys urgency or time-sensitivity" } } }
The response returns all three under `answers`, keyed by the names you chose:
{ "model": "jev-1.13.0", "answers": { "department": { "type": "choice", "choice": "technical", "confidence": 0.78, "probabilities": { "technical": 0.85, "sales": 0.0, "billing": 0.15 } }, "frustration": { "type": "score", "score": 1.0, "confidence": 1.0, "legend": { "0": "Calm, just stating facts", "1": "Frustrated but civil", "2": "Very angry, strong language" }, "probabilities": { "0": 0.0, "1": 1.0, "2": 0.0 } }, "is_urgent": { "type": "noul", "noul": 1.0 } }, "usage": { "input_tokens": 392, "output_tokens": 65 } }
Batching is the cost lever. Three questions in one request is one input pass and one `input_tokens` figure, and `usage` comes back on every response so you can meter it yourself.
The goal is a decision your code can act on, with a band it refuses to guess in.
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient STATE = "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP." QUESTIONS = { "department": Choice( instructions="Which team should handle this", criteria={ "billing": "Payment or subscription issues", "technical": "Bugs or integration problems", "sales": "Pricing or account questions", }, ), "frustration": Score( instructions="How frustrated the customer appears", criteria=[ "Calm, just stating facts", "Frustrated but civil", "Very angry, strong language", ], ), "is_urgent": Noul( instructions="The message conveys urgency or time-sensitivity", ), } HIGH = 0.85 LOW = 0.60 def triage(state: str) -> dict: with TypeSafeClient() as client: response = client.system_one(model="jev-latest", state=state, questions=QUESTIONS) department = response.answers["department"] frustration = response.answers["frustration"] urgent = response.answers["is_urgent"].noul if department.confidence < LOW: return {"route": "human_review", "why": "department confidence too low"} if LOW <= urgent <= HIGH: return {"route": "human_review", "why": "urgency is ambiguous"} queue = "priority" if (urgent > HIGH and frustration.score >= 1.0) else "standard" return { "route": department.choice, "queue": queue, "urgency": urgent, "frustration": frustration.score, "confidence": department.confidence, "probabilities": department.probabilities, "usage": response.usage, } if __name__ == "__main__": print(triage(STATE))
With the documented response for that ticket, `department.choice` is `technical` at 0.85, `frustration.score` is 1.0, and `is_urgent.noul` is 1.0, so the ticket routes to `technical` in the `priority` queue. Change the state from a message about losing sales to a calm feature request and the urgency answer moves, which is the behaviour you are buying.
Three things in that function are deliberate. The review band is checked before any routing. The `probabilities` map travels with the decision so a reviewer can see the near miss. And `usage` is returned alongside the decision so the cost of the judgment sits next to the thing it decided.
A green checkmark is not verification. This helper makes one call and asserts the contract, so a misconfigured environment fails loudly instead of returning an empty answer:
from typesafe_sdk import Noul, TypeSafeClient def verify_install() -> None: with TypeSafeClient() as client: response = client.system_one( model="jev-latest", state="This is a connectivity check.", questions={ "is_test": Noul(instructions="Is this message a connectivity check?"), }, ) assert response.model, "no model name returned" answer = response.answers["is_test"] assert answer.type == "noul", f"unexpected answer type: {answer.type}" assert 0.0 <= answer.noul <= 1.0, f"noul out of range: {answer.noul}" assert response.usage.input_tokens > 0, "no input tokens reported" print(f"ok: model={response.model} noul={answer.noul} usage={response.usage}") if __name__ == "__main__": verify_install()
Run it after any credential change. If `TYPESAFE_API_KEY` is missing, the SDK raises rather than returning a default, and the status codes in the next section tell you which failure you have.
A score with low confidence is not a worse answer, it is a different instruction: stop. This is the pattern that keeps a judgment out of a consequential path until it has earned its place.
from typesafe_sdk import Score, TypeSafeClient STATE = "The export button crashes the settings page in Safari. It works in Chrome, but a few of our customers only use Safari." CRITERIA = [ "Cosmetic; no impact to functionality", "Broken or degraded feature, but workaround exists", "Blocking issue; no workaround exists", ] MIN_CONFIDENCE = 0.60 def severity(state: str) -> dict: with TypeSafeClient() as client: response = client.system_one( model="jev-latest", state=state, questions={ "bug_severity": Score( instructions="How severe is the reported issue?", criteria=CRITERIA, ), }, ) answer = response.answers["bug_severity"] if answer.confidence < MIN_CONFIDENCE: return { "action": "human_review", "score": answer.score, "confidence": answer.confidence, "probabilities": answer.probabilities, } level = round(answer.score) return { "action": "auto_label", "severity": answer.legend[str(level)], "score": answer.score, "confidence": answer.confidence, }
The documented response for that bug report is `score` 1.43 with `confidence` 0.35 and probabilities `0.0`, `0.57`, `0.43`. Under the threshold above this returns `human_review` and hands a person the whole distribution rather than a label. That is the right outcome for an ambiguous report, and it is only visible because the response carries `confidence` and `probabilities` instead of a single number.
Every body below was captured by calling the live endpoint on October 12, 2026. The envelope is consistent: `detail.error_type` names the class of failure and `detail.message` is written for a human.
No Authorization header at all. HTTP 403, which surprises people who expect 401:
{"detail":{"error_type":"authentication_error","message":"Must supply an API key! Check your request and try again."}}
A key that is present but wrong, including a key with trailing whitespace or one from a different environment. HTTP 401:
{"detail":{"error_type":"authentication_error","message":"Cannot authenticate with the server. Please check your API key and try again."}}
Malformed JSON in the body. HTTP 422, and it uses the validation envelope rather than the authentication one:
{"detail":[{"type":"json_invalid","loc":["body",1],"msg":"JSON decode error","input":{},"ctx":{"error":"Expecting property name enclosed in double quotes"}}]}
The wrong verb. HTTP 405, and the shape changes again because the framework raises it:
{"detail":"Method Not Allowed"}
Handle them by class rather than by matching strings:
import httpx from typesafe_sdk import Noul, TypeSafeClient def ask(state: str) -> dict: try: with TypeSafeClient() as client: response = client.system_one( model="jev-latest", state=state, questions={"is_urgent": Noul(instructions="Does this convey urgency?")}, ) return {"ok": True, "noul": response.answers["is_urgent"].noul} except httpx.HTTPStatusError as err: status = err.response.status_code detail = err.response.json().get("detail") if status in (401, 403): # Configuration, not traffic. Alert, do not retry. return {"ok": False, "fatal": True, "status": status, "detail": detail} if status == 422: # Your payload is wrong. A bug in your code, not a bad request to retry. return {"ok": False, "fatal": True, "status": status, "detail": detail} if status == 429 or status >= 500: # Retry with backoff; the SDK can also be given its own retry policy. return {"ok": False, "retry": True, "status": status} return {"ok": False, "fatal": True, "status": status, "detail": detail}
One honest gap. This page does not show a 400 body, because the API did not use 400 for any of those failures: it returned 403 for a missing key, 401 for a bad key, 422 for a malformed body and 405 for the wrong method. The case that would produce a validation error is a semantically valid request with a bad payload, such as an empty `questions` object, and reproducing it needs a valid key. Probe it yourself in one command once you have one, and read `detail` rather than guessing:
curl -s -X POST https://api.typesafe.ai/v1/systemone \ -H "Authorization: Bearer $TYPESAFE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"state":"probe","model":"jev-latest","questions":{}}' | head -c 400
Do not declare success because a command exited zero.
The reason to assert the shape is that the useful output is not prose. A response returning `technical` with a probability per option is something your code can branch on. If you are parsing a sentence to find the answer, you are on the wrong route.
Mixing the skill and the SDK in your head is the first. The skill changes how your agent writes code; it does not make Jev calls for you and it does not remove the need for a key at runtime. Teams install the skill, write a call, and then debug a 403 that is really a missing environment variable. The Claude Code tutorial walks a working example from install to measured result.
Version drift is the second. The skills repository and both SDKs move, and an install from weeks ago will not know that. The plugins guide covers how to audit what you installed, and the complete guide indexes the whole set.
A question people ask before installing anything: is Jev replacing the coding model. It is not. TypeSafe states plainly that Jev is not a drop-in replacement for a coding LLM, and hosts like OpenCode keep their own model for code while Jev handles narrow judgments alongside it. The OpenCode walkthrough covers the host-specific configuration, and the compaction comparison and routing explainer cover the two places that distinction bites hardest.
Do I need an API key to use the skill?
Yes, at the moment your code runs. The skill teaches the agent the interface; the key authenticates the call your code makes.
Can I install the skill in more than one agent?
Yes, per agent. Run the platform-specific command in each one, and use one method per agent rather than stacking a plugin and a skill install in the same place.
Which SDK should I start with?
Whichever language your service already uses. Both official clients expose the same shapes, so the choice does not lock you in.
Does Jev send my data anywhere?
Your state goes to the endpoint you call, which is why the install decision and the data decision are separate. Treat the key as a server-side credential, send the narrowest state the judgment needs, and see how to gate risky tool calls for keeping side effects in your code.
Why did my call return a score of 1.43?
Because a score is a position on your rubric rather than an integer index. Round it for a label, or keep the fraction and use the probabilities for anything consequential.
Installing an agent skill takes ten minutes, and so does the first typed call. Deciding which judgments in your product deserve a threshold, what the review band should be, and what happens when the key rotates is the part that takes engineering judgment, and it is the part where a hundred small choices decide whether the feature is maintainable in six months.
Maxpertise embeds an AI engineer inside your existing engineering team for that work, spec-driven, reviewed before anything lands, with a clean exit so you keep the code. Start with what an AI engineer costs, or send a written intake and get a proposal back.
[1] TypeSafe, Quickstart, accessed October 12, 2026. The endpoint `POST https://api.typesafe.ai/v1/systemone`, the `Authorization: Bearer ` header, the batched three-question payload of `state`, `model` and `questions`, and the runnable curl form reproduced here.
[2] TypeSafe, Noul reference, accessed October 12, 2026. The `is_human_escalation` 0.99 and `is_repeat_contact` 0.93 response, the `NoulCriteria` true and false form, and the threshold snippet.
[3] TypeSafe, Choice reference, accessed October 12, 2026. The returns, shipping and billing criteria example and its response, including `confidence` alongside per-option `probabilities`.
[4] TypeSafe, Score reference, accessed October 12, 2026. The Safari bug report example, and the `score` 1.43 with `confidence` 0.35 and the `legend` field.
[5] TypeSafe, Primitives overview, accessed October 12, 2026. The three question shapes and their result shapes.
[6] TypeSafe, Agent skill guide, accessed October 12, 2026. Both install commands, the instruction to use one installation method, and the update routes for each method.
[7] TypeSafe, typesafe-ai/skills, accessed October 12, 2026. The install section and the single-skill table.
[8] TypeSafe, typesafe-sdk-js, accessed October 12, 2026. `npm install @typesafe-ai/sdk`, the Node 20 requirement, and the `choice` helper example reproduced here.
[9] TypeSafe, typesafe-sdk-python, accessed October 12, 2026. `pip install typesafe-sdk` and the synchronous and asynchronous `TypeSafeClient`.
[10] Live endpoint probes, October 12, 2026. Every error body in the error section was captured by calling `https://api.typesafe.ai/v1/systemone` directly that day: 403 with no Authorization header, 401 with a bearer token that is not a valid key, 422 with an unparseable JSON body, and 405 for a GET.
Maxpertise is an AI-native engineering company. We embed native AI engineers inside your team, live in about 10 days. Please enable JavaScript to view the site, or email contact@maxpertise.net.