Documentation
Jev Screen is a judgment API for hiring: you send a CV and a role profile, you get scored dimensions, a composite, a confidence value, and a tier. The service is stateless — it scores, returns, and stores no applicant data. The full REST contract is machine-readable at /openapi.json (OpenAPI 3.1 — generate a client for any stack with openapi-generator, kiota, or your IDE).
Connect an AI agent (MCP)
The API speaks MCP over streamable HTTP at https://dev-jev.arsana.cloud/mcp. Agents authenticate with the same X-API-Key. Three tools: screen_candidate, list_roles, get_role.
Claude Code:
claude mcp add --transport http jev-screen https://dev-jev.arsana.cloud/mcp --header "X-API-Key: jsk_YOUR_KEY"
Cursor or any MCP client (streamable HTTP):
{
"mcpServers": {
"jev-screen": {
"type": "http",
"url": "https://dev-jev.arsana.cloud/mcp",
"headers": { "X-API-Key": "jsk_YOUR_KEY" }
}
}
}
Quickstart: any stack
curl:
curl -X POST https://dev-jev.arsana.cloud/v1/screen \
-H "Content-Type: application/json" \
-H "X-API-Key: jsk_YOUR_KEY" \
-d '{"role":"ai-community-admin","applicant":{"cv_text":"full CV text..."}}'
Node 20+ (fetch built in):
const r = await fetch("https://dev-jev.arsana.cloud/v1/screen", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": process.env.JEV_KEY },
body: JSON.stringify({ role: "ai-community-admin", applicant: { cv_text } }),
});
const { composite, confidence, tier } = await r.json();
Python:
import os, requests
r = requests.post(
"https://dev-jev.arsana.cloud/v1/screen",
headers={"X-API-Key": os.environ["JEV_KEY"]},
json={"role": "ai-community-admin", "applicant": {"cv_text": cv_text}},
)
print(r.json()["tier"], r.json()["composite"])
A typical response, in about a second:
{
"role": "ai-community-admin",
"model": "jev-1.13.0",
"usage": { "input_tokens": 1041, "output_tokens": 115 },
"redacted": true,
"dimensions": {
"ai_depth": { "type": "score", "score": 3.1, "normalized": 0.77, "confidence": 0.93 },
"community_exp": { "type": "noul", "probability": 0.81 },
"communication": { "type": "score", "score": 2.8, "normalized": 0.93, "confidence": 0.9 },
"self_direction": { "type": "noul", "probability": 0.88 },
"red_flags": { "type": "choice", "choice": "none", "probabilities": { "none": 0.96 } }
},
"composite": 0.84,
"confidence": 0.9,
"tier": "shortlist",
"tier_reason": "composite 0.84 >= 0.7 with sufficient confidence"
}
Endpoints
| Method and path | Auth | What it does |
|---|---|---|
| GET /health | none | Liveness probe. |
| GET /v1/roles | API key | List available role profiles with dimensions and weights. |
| GET /v1/roles/:id | API key | One role profile in full. |
| POST /v1/screen | API key | Screen one applicant. The main endpoint. |
Request fields
- role — a role profile id, or pass an inline profile object with the same shape instead.
- applicant.cv_text — required, plain text CV. Minimum 200 characters; a shorter text returns 422 because the extraction probably failed.
- applicant.form_answers — optional structured answers, passed to the model as context.
- redact — default true; strips emails, phone numbers, and URLs from the CV before the model sees it.
Reading the answer
- Score dimensions carry normalized (0 to 1) and a confidence (how concentrated the model's answer distribution was).
- Noul dimensions carry a probability of yes. By design they have no separate confidence.
- composite is the weighted mean of normalized values, using the role profile's weights.
- tier is one of shortlist, review, weak. It is computed by arithmetic in the service, not by the model. Low confidence always routes to review. The API never returns "reject" — your policy decides what to do with a weak.
Role profiles
A role profile is JSON: questions (each a noul yes/no, score rubric, or choice between options), weights over noul/score answers, and a policy block (thresholds shortlist_above, weak_below, min_confidence, and consistent_low_keys). Shipped profile: ai-community-admin. Inline profiles let you experiment without waiting for us.
Batch screening
Up to 25 applicants in one request; items run in parallel and results come back index-aligned. A bad CV returns an in-band error object for its slot — one failure never kills the batch. Each applicant counts as one screen against quota.
POST /v1/screen/batch
{
"role": "ai-community-admin",
"applicants": [
{ "cv_text": "first CV..." },
{ "cv_text": "second CV..." }
]
}
Spreadsheets and forms (no code)
Excel or any CSV: sign in, open Import CSV in the dashboard, upload your file, and download the same rows with composite, confidence, tier, and per-dimension score columns added. Name the CV column cv (or any column of full CV text works — it is auto-detected).
Google Sheets: in your sheet, open Extensions > Apps Script, paste the script below, set your API key, save. A Jev Screen menu appears on reload; select the CV column range and run Screen selected rows. Results are written into new columns.
// Apps Script — Google Sheets. Extensions > Apps Script > paste > save.
const JEV_URL = "https://dev-jev.arsana.cloud/v1/screen";
const JEV_KEY = "jsk_YOUR_KEY";
const ROLE = "ai-community-admin";
function onOpen() {
SpreadsheetApp.getUi().createMenu("Jev Screen")
.addItem("Screen selected rows", "screenSelection")
.addToUi();
}
function screenSelection() {
const sheet = SpreadsheetApp.getActiveSheet();
const range = sheet.getActiveRange(); // select the CV cells first
const cvValues = range.getValues();
const out = [];
for (let i = 0; i < cvValues.length; i++) {
const cv = String(cvValues[i][0] || "").trim();
if (cv.length < 200) { out.push(["extraction_failed"]); continue; }
const r = UrlFetchApp.fetch(JEV_URL, {
method: "post",
contentType: "application/json",
headers: { "X-API-Key": JEV_KEY },
payload: JSON.stringify({ role: ROLE, applicant: { cv_text: cv } }),
muteHttpExceptions: true,
});
if (r.getResponseCode() !== 200) { out.push(["error " + r.getResponseCode()]); continue; }
const j = JSON.parse(r.getContentText());
out.push([j.tier, j.composite, j.confidence]);
}
// write tier / composite / confidence next to the selection
sheet.getRange(range.getRow(), range.getColumn() + 1, out.length, 3).setValues(
out.map((r) => [r[0], r[1] ?? "", r[2] ?? ""])
);
}
Google Forms: keep your form asking for name + a long-answer "paste your CV" question, link responses to a Sheet, then add this to the same Apps Script project and set an onFormSubmit installable trigger (Triggers > Add trigger > function onFormSubmit). Each submission is screened the moment it lands; the tier appears in the response sheet automatically.
function onFormSubmit(e) {
const itemResponses = e.response.getItemResponses();
let cv = "";
for (const ir of itemResponses) {
const t = ir.getItem().getTitle().toLowerCase();
if (t.includes("cv") || t.includes("resume")) cv = String(ir.getResponse() || "");
}
if (cv.trim().length < 200) return;
const r = UrlFetchApp.fetch(JEV_URL, {
method: "post",
contentType: "application/json",
headers: { "X-API-Key": JEV_KEY },
payload: JSON.stringify({ role: ROLE, applicant: { cv_text: cv } }),
muteHttpExceptions: true,
});
if (r.getResponseCode() !== 200) return;
const j = JSON.parse(r.getContentText());
const sheet = e.range.getSheet();
sheet.getRange(e.range.getRow(), e.range.getLastColumn() + 1, 1, 3)
.setValues([[j.tier, j.composite, j.confidence]]);
}
Concepts
- Score dimension — rubric with ordered levels (0 = no evidence, top = strongest evidence). The answer carries normalized (0-1) and confidence.
- Noul dimension — a yes/no judgment returned as the probability the condition holds. No separate confidence by design.
- Choice dimension — picks one option from a defined set (used for red flags). Reported, not part of the composite.
- composite — weighted mean of normalized scores and noul probabilities, using the role profile's weights.
- confidence — the minimum confidence across score dimensions. Below the role's floor the tier is review, always. The system would rather hand you an uncertain candidate than mislabel one.
Tier policy, with worked examples
Tier is arithmetic in code, not model output. For every profile:
if (confidence < min_confidence) return "review"; // the gate. always.
if (composite >= shortlist_above) return "shortlist";
if (composite < weak_below
&& all consistent_low_keys low) return "weak";
return "review";
| Case | Composite | Confidence | Result | Why |
|---|---|---|---|---|
| A | 0.84 | 0.90 | shortlist | Above 0.7 with confident scores |
| B | 0.30 | 0.80 | weak | Below 0.35, confident, and must-have nouls agree low |
| C | 0.25 | 0.40 | review | Low composite but the model is unsure — a human looks, always |
| D | 0.91 | 0.52 | review | High composite, unsure model — never auto-shortlisted |
weak is rare by design: it needs low composite AND sufficient confidence AND the consistency checks. Recommended handling: shortlist = advance; review = human screen; weak = keep on file or archive — your policy decides. The API never returns reject.
Reliability features
- Request ids — send X-Request-Id (8-64 chars) or let us mint one; echoed as a response header and in every JSON body as request_id. Quote it in support conversations.
- Sandbox mode — header X-Sandbox: true on screen endpoints returns a deterministic sample response. No model call, no quota. Build and test your whole integration for free.
- Idempotency — send Idempotency-Key on POST /v1/screen or /batch; a retry within 24 hours returns the stored response instead of double-screening (and double-charging quota).
- Burst limiting — 5 requests/second per key. Exceeding it returns 429 with a Retry-After header, distinct from monthly-quota 429s.
Cost math
Pro is $29 for 2,500 screens: about 1.2 US cents per screen. A 400-applicant opening costs roughly $4.70 to triage on Pro. The free tier's 100 screens is enough to calibrate one role before paying anything.
Authoring a role profile
A profile is JSON: questions (each noul / score / choice), weights over noul+score answers, and a policy block. The craft rules:
- One narrow judgment per question. Bad: "Is this candidate a strong communicator?" Good: "How clear and structured is the written communication in this CV?" — the CV is the evidence, the rubric grades evidence, not the person.
- Concrete rubric levels. Each level describes a situation a reader could point at in a CV ("describes building with AI regularly"), never comparative adjectives ("excellent").
- Level 0 = no evidence. Absence of evidence must score low, not make the model guess.
- Every choice includes a none/no-match option. The model cannot choose an omitted value.
- Sparse CVs should land in review via low confidence — that is correct behavior, not a bug.
- Test inline first (pass profile instead of role), calibrate on 30+ labeled CVs, then ask us to ship it as a first-class profile.
Calibrating on your own data
- Collect 30+ past CVs with the human decision (advance / reject / interview).
- Screen them through the API. Do not look at the results while collecting human labels.
- Compute disagreement, focusing on false-weak: candidates humans advanced but the API called weak. This is the only error class that matters ethically.
- Tune question wording first, thresholds second. Re-run.
- Keep shadow mode — humans confirm every weak — until false-weak is zero on two consecutive sets.
Frequently asked, honestly answered
- Is it biased against groups? Rubrics judge evidenced behavior, not identity; contact details are stripped before scoring; language dimensions judge writing, never names or schools. The real control is your calibration set — that is why the protocol above is public.
- Is it legal in my jurisdiction? Under NYC LL144 the AEDT operator is whoever acts on the tool's output; Jev returns scores and never rejects, keeping the human decision with you (bias-audit support on request). Indonesia's PDP law: consent + data minimization, aided by redaction and zero retention. You remain the data controller for CVs you send.
- Why not just prompt ChatGPT? Typed answers with probabilities, tier arithmetic in code, a confidence gate, redaction, zero retention, metered quota, an SLA-able API — versus parsing prose and hoping.
- What if it is wrong? Tiers are inputs to your policy, review is the catch-all for uncertainty, and the calibration protocol measures error before you automate anything.
- Where does candidate data go? Nowhere for long: scored in memory, returned, not stored; logs record paths only. See Privacy and data handling.
- Do candidates need to know? Best practice is disclosure in your application flow ("applications are screened with software assistance; a human makes all decisions"). We provide notice text you can copy.
- Latency and volume? A single screen returns in seconds; batches of 25 run in parallel; Scale covers 15,000 screens/month. Larger async jobs are on the roadmap.
- What does a screen cost? ~1.2 cents on Pro; free tier for calibration.
- Bahasa Indonesia CVs? Yes — the judgment model reads Indonesian and English. Several role profiles (telesales, customer support) are written for the SEA market specifically.
- Can we audit it? Public rubrics (this page), per-dimension scores and tier_reason on every response, and the model version stamped in each answer. Pin it in your records.
Errors and recovery
| Status | Meaning | Recovery |
|---|---|---|
| 400 | Malformed payload (missing role/profile, batch over 25, bad Idempotency-Key) | Fix the request shape; chunk batches client-side |
| 401 | Missing or invalid X-API-Key | Check the key in your dashboard; was it revoked? |
| 404 | Unknown role id | GET /v1/roles lists valid ids |
| 422 | cv_text under 200 characters | Extraction failed upstream — route that applicant to human review. Never pad the text to pass the gate |
| 429 + Retry-After | Burst limit (5 req/s per key) | Wait the stated seconds; this is not your monthly quota |
| 429, quota message | Monthly screens exhausted | Check the dashboard usage meter; upgrade or wait for the month to reset |
| 502 | Judgment model unreachable | Safe to retry the same payload (stateless); use Idempotency-Key to avoid double-charging |
Failed requests (any error before a completed judgment) do not consume quota. Batch items that fail return in-band error objects, index-aligned with your request — handle per item.
Rate limits and quotas
Quota is per account across all keys, measured in screens per calendar month: 100 on Free, 2,500 on Pro, 15,000 on Scale. Over-quota requests return 429 with a JSON body. Requests time out after 10 seconds per attempt.
Privacy notes
- The service stores nothing: no CV text, no scores, no applicant identifiers.
- Contact channels are redacted from CV text before anything leaves the service.
- Request logs record the method and path only.
- Usage metering records key id, token counts, and tier — never CV content.