Jev API Tutorial: Make Your First Call in 10 Minutes

Updated Applies to Jev 1.13

You have an API key and ten minutes — that is genuinely all a first Jev call takes. Because the model’s main demo channel is OpenRouter’s OpenAI-compatible endpoint, the request is the same shape you already send to chat models; what changes is what comes back. This tutorial walks through a complete, copy-pasteable call in curl and Python, explains every field, lists every error that catches first-timers, and ends with the five checks you should make before any call goes near production.

TL;DR: POST to https://openrouter.ai/api/v1/chat/completions with Authorization: Bearer <key>, the Jev model id, and your question phrased as a judgment, choice or scoring question. The answer comes back inside the standard OpenAI-compatible envelope as structured JSON — an answer, a confidence score and a rationale. Parse the message content as JSON and read the fields.

From zero to first call: a 7-step runbook

This runbook assumes you have nothing yet — no account, no key, no code. Follow the steps in order; each step has a concrete expected output so you know when to proceed.

StepActionUI path / fieldExpected output
1Create an OpenRouter accountopenrouter.ai → Sign UpConfirmation email received and account active
2Add creditsDashboard → Credits → Add Credits → choose amount → payCredits balance shows non-zero in the top bar
3Create an API keyDashboard → Keys → Create Key → set a name and optional spend limitA key string starting with sk-or- copied to clipboard
4Store the key in your environmentexport OPENROUTER_API_KEY="sk-or-…" in your shell profileecho $OPENROUTER_API_KEY prints the key without None
5Confirm the current model slugopenrouter.ai → Models → search “jev” → open the model pageExact slug string, e.g. typesafe/jev-1.13 visible
6Send the test callCopy the curl command from the next section; paste and runHTTP 200 and a JSON response body in the terminal
7Parse the answerExtract choices[0].message.content; run json.loads on itA dict with answer, confidence and rationale fields

Two things account for almost every failure in step 6: the key is in the clipboard but not exported to the environment, and the model slug is different from what the guide shows. Fix those two before debugging anything else. A quick sanity check before step 6: open a fresh terminal, run echo $OPENROUTER_API_KEY and echo $JEV_MODEL_SLUG (if you stored the slug separately), and confirm both print non-empty strings. If either is blank, the following call will fail with a 401 or 404 respectively — no amount of request debugging will fix a missing environment variable.

The request, field by field

A first call is a judgment question — the simplest of the three primitives. Here is the minimal working curl:

# Confirm the exact model slug on the OpenRouter model page before running
curl https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "typesafe/jev-1.13",
    "messages": [
      {
        "role": "user",
        "content": "Judgment question: does this comment contain spam? Comment: \"Great post, check my site for cheap meds\""
      }
    ]
  }'

What each part does:

PartValuePurpose
URLhttps://openrouter.ai/api/v1/chat/completionsOpenRouter’s OpenAI-compatible endpoint — no path variation needed
Authorization headerBearer $OPENROUTER_API_KEYReads the key from the environment variable; never paste raw keys in scripts
Content-Typeapplication/jsonTells the server the body is JSON; omitting this causes a 400
modeltypesafe/jev-1.13The Jev model id — confirm the exact slug on the OpenRouter model page
messages[0].roleuserA single-turn question; Jev does not carry conversation state
messages[0].contentYour questionPhrase it as judgment / choice / scoring and embed the input inline

Note what is not required: no special “judgment mode” parameter, no bespoke endpoint, no session token. The question type is carried entirely by how you phrase the content field. For the official TypeSafe AI channel, confirm the actual endpoint and fields in the official documentation at typesafe.ai.

The response, decoded

The HTTP response is the standard OpenAI-compatible envelope. The outer object contains a choices array; the first element’s message.content holds the structured answer as a JSON string. Inside that string — example fixture, confirm exact field names in the official documentation:

{
  "answer": "yes",
  "confidence": 0.97,
  "rationale": "The comment promotes an unrelated pharmaceutical site, a classic spam pattern."
}

Reading the fields:

The usage object in the envelope — present when you add "usage": {"include": true} to the request body — reports prompt_tokens, completion_tokens and an estimated cost. Read this section on measuring cost and latency for how to use it.

The full extraction path in pseudocode:

envelope  = JSON parse HTTP body
content   = envelope["choices"][0]["message"]["content"]
result    = JSON parse content          ← the line people forget
answer    = result["answer"]

The same call in Python

import json, os, requests

resp = requests.post(
    "https://openrouter.ai/api/v1/chat/completions",
    headers={
        "Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        # Confirm the exact model slug on the OpenRouter model page
        "model": "typesafe/jev-1.13",
        "messages": [{
            "role": "user",
            "content": (
                "Judgment question: does this comment contain spam? "
                'Comment: "Great post, check my site for cheap meds"'
            ),
        }],
        "usage": {"include": True},   # returns token counts and estimated cost
    },
    timeout=30,
)
resp.raise_for_status()                 # raises on 4xx/5xx before we parse
envelope = resp.json()
result = json.loads(envelope["choices"][0]["message"]["content"])

print(result["answer"], result["confidence"])

# Optional: log cost for the first few calls to calibrate
usage = envelope.get("usage", {})
print(f"tokens: {usage.get('prompt_tokens')}+{usage.get('completion_tokens')}")

Three implementation notes worth reading before you copy-paste:

  1. resp.raise_for_status() must come before resp.json() or a failed request body might parse as valid JSON that silently swallows the error code.
  2. The json= parameter in requests.post both serialises the body and sets Content-Type: application/json automatically — you do not need a separate header for it, but the header in the example makes the intent explicit for readers.
  3. json.loads(envelope["choices"][0]["message"]["content"]) is the line that catches everyone. The message content is a string containing JSON, not an already-parsed dict. If you skip this step you will get a TypeError or a string instead of a dict, depending on how you access it.

Failure modes: what goes wrong and why

Four HTTP status codes cover essentially every first-call failure. The table below gives the cause, how to confirm it, and what to fix:

CodeMost common causeHow to confirmFix
401 UnauthorizedKey missing from environment, wrong env var name, or key revokedprint(os.environ.get("OPENROUTER_API_KEY")) — if None, the export did not run in this shellRe-export the key; check that the var name matches exactly (case-sensitive)
403 ForbiddenKey exists but has no credits, or key has a spend limit that is already reachedLog into the OpenRouter dashboard → Credits; check remaining balance and per-key limitsAdd credits, or raise the key’s spend cap
404 Not FoundModel slug is wrong, outdated, or typed with a typoCompare the model value in your request to the exact string on the OpenRouter model pageCopy the slug fresh from the model page; do not rely on a cached value
429 Too Many RequestsConcurrency or per-minute request rate exceeds the account’s tier limitThe response body usually includes a Retry-After headerBack off exponentially; reduce max_workers; check your tier’s documented limits
400 Bad RequestMissing or malformed JSON body; Content-Type header absent; model field missing entirelyRead the response body — it usually names the missing fieldValidate the JSON body before sending; confirm all required fields are present
TypeError / JSONDecodeError in clientjson.loads was skipped on message content, or the content was not valid JSON for this callPrint the raw message.content string before parsingWrap the json.loads call in a try/except; on parse failure, log the raw content and retry once

One subtle failure: the key is correctly exported but the env var name has a typo. For example OPENROUTER_APIKEY instead of OPENROUTER_API_KEY. Python’s os.environ['OPENROUTER_API_KEY'] raises KeyError immediately; os.environ.get('OPENROUTER_API_KEY') returns None silently and then fails when the header is built. Check the name character by character if a key that “should be there” keeps returning None.

Measuring cost and latency on the first call

Do not wait for your first invoice to understand what a Jev call costs. On the very first run, add "usage": {"include": True} to the request body and read the envelope before parsing the answer:

envelope = resp.json()

usage = envelope.get("usage", {})
prompt_tokens      = usage.get("prompt_tokens", 0)
completion_tokens  = usage.get("completion_tokens", 0)
# OpenRouter includes an estimated cost field — confirm the field name on the model page
estimated_cost_usd = usage.get("total_cost", 0)

print(f"prompt={prompt_tokens} completion={completion_tokens} cost≈${estimated_cost_usd:.6f}")

For latency, use Python’s time.perf_counter() around the request and run the same prompt three times back-to-back:

import time

times = []
for i in range(3):
    t0 = time.perf_counter()
    resp = requests.post(URL, headers=HEADERS, json=BODY, timeout=30)
    resp.raise_for_status()
    times.append(time.perf_counter() - t0)

print(f"latency: {min(times):.2f}s / {sum(times)/3:.2f}s / {max(times):.2f}s (min/avg/max)")

Three data points are not a benchmark, but they tell you whether your prompt is costing tenths of cents or whole cents, and whether latency is sub-second or multi-second — both of which affect how you design the pipeline. The cost-and-latency guide covers the full measurement methodology once you move past the prototype.

Two things to log from the start even before you care about cost optimisation: the prompt_tokens per call (it tells you whether prompt size is drifting as inputs change) and the wall-clock latency per call (p95 is the number that matters for user-facing pipelines). Set up these logs on day one; retrofitting observability into an already-running pipeline is always harder.

Common question-wording mistakes

Getting the request to return HTTP 200 is step one. Getting a useful answer is step two, and question wording is where most debugging time goes. Three patterns cause most wording errors:

Forgetting to embed the input. The question must contain the thing being judged. "Judgment question: is this spam?" is unanswerable without the text to evaluate. The full form is "Judgment question: is this spam? Text: \"…\"". A useful habit: write the question template first with a {input} placeholder, then format it before every call — this prevents the accidental omission and also makes the template unit-testable independently of the API.

Phrasing a judgment question as a chat request. "Tell me whether this is spam" is an instruction to a chat assistant. Jev is not a chat assistant — it answers typed questions. Use the explicit question form: "Judgment question: does this text contain spam? Text: \"…\"".

Mixing question types in one message. Asking "Is this spam? And if so, rate the severity from 1–10" in a single content field blends a judgment and a scoring question. Send two separate calls, one per question type, and combine the results in your own code. The three-primitives guide covers each question type in full, including how to chain them.

A useful debugging loop: when a call returns confidence below 0.7, read the rationale field. It usually tells you exactly what the model was uncertain about — missing context, ambiguous phrasing, or an input that genuinely straddles two categories. Adjust the question wording and rerun; most low-confidence results clear up in one or two iterations.

Before you go to production: a 5-point checklist

These five checks take under fifteen minutes but prevent the five most common production incidents:

  1. Key is in an environment variable, not in source code. Scan your repo for the sk-or- prefix before any commit. A leaked key can be drained by a crawler within minutes of appearing in a public repository.

  2. The model slug is read from config, not hardcoded. Put the slug in an environment variable (JEV_MODEL_SLUG) so a version change is one config update, not a code deploy. Confirm the current slug on the OpenRouter model page at least once before launch.

  3. There is a spend cap on the key. Set a per-key spend limit in the OpenRouter dashboard. A retry loop bug or a sudden traffic spike should exhaust the key limit, not the account balance.

  4. raise_for_status() is called before parsing. Any code path that calls resp.json() without first checking the status code will silently swallow errors. The failure only surfaces later as a confusing KeyError or AttributeError when the answer is accessed.

  5. The json.loads on message content is wrapped in a try/except. Jev returns structured JSON reliably, but network retries, model version changes and edge-case inputs can all produce unexpected content. Log the raw string on parse failure before retrying or alerting — you will need it to debug.

Once all five are green, the confidence-and-fallback guide covers the next layer: choosing a confidence threshold, routing low-confidence results to human review, and measuring the real-world precision/recall of your threshold choice.

What comes next

This guide covers the mechanics of a single call. The guides that follow cover what you build on top:

GuideWhat it covers
Three primitivesJudgment, choice and scoring question types; when to use each; how to chain them
API channelsOpenRouter vs the official TypeSafe AI channel; one adapter that covers both; migration runbook
Confidence and fallbackChoosing a confidence threshold; routing low-confidence results to human review
Cost and latencyMeasuring per-call economics; prompt-size effect; batching and concurrency trade-offs
TroubleshootingFull error-to-fix table beyond the four codes in this guide

The spam comment detection case shows this exact call wired into a moderation pipeline end to end — a useful read once the mechanics above are working and you want to see how the runbook translates into a real production integration with retry logic, logging, and a human review queue.

This guide applies to Jev 1.13. If your model page shows a different version, the request shape remains the same — only the slug and response fixture field names may differ; confirm those in the current official documentation.

Frequently asked questions

+ What endpoint do I use for my first Jev call?

The main demo channel is OpenRouter's OpenAI-compatible chat completions endpoint at https://openrouter.ai/api/v1/chat/completions. Official TypeSafe AI endpoints are documented on typesafe.ai — confirm them there rather than guessing.

+ What model id do I pass?

This guide uses typesafe/jev-1.13, but slugs change with versions — always confirm the exact model slug on the OpenRouter model page before wiring it into code.

+ Why does my first call fail with a JSON error?

Usually because the message content was not parsed as JSON before use, or the model id is wrong. The content field is a JSON string, not a dict — pass it through json.loads() and check the slug first.

+ How do I measure what a single call actually costs?

Add usage: {include: true} to your OpenRouter request body. The response envelope includes a usage object with prompt_tokens, completion_tokens and, for OpenRouter, an estimated cost field. Log these on your first ten calls to calibrate expectations before scaling.

+ How many requests per second can I send before hitting rate limits?

This depends on your OpenRouter credit tier, not a fixed number. Start with a concurrency of 5 and watch for 429 responses; step up by 5 and re-measure. Do the same exercise after any channel switch — limits are per-channel, not universal.

Keep reading