jevscreen

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 pathAuthWhat it does
GET /healthnoneLiveness probe.
GET /v1/rolesAPI keyList available role profiles with dimensions and weights.
GET /v1/roles/:idAPI keyOne role profile in full.
POST /v1/screenAPI keyScreen one applicant. The main endpoint.

Request fields

Reading the answer

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

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";
CaseCompositeConfidenceResultWhy
A0.840.90shortlistAbove 0.7 with confident scores
B0.300.80weakBelow 0.35, confident, and must-have nouls agree low
C0.250.40reviewLow composite but the model is unsure — a human looks, always
D0.910.52reviewHigh 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

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:

Calibrating on your own data

  1. Collect 30+ past CVs with the human decision (advance / reject / interview).
  2. Screen them through the API. Do not look at the results while collecting human labels.
  3. Compute disagreement, focusing on false-weak: candidates humans advanced but the API called weak. This is the only error class that matters ethically.
  4. Tune question wording first, thresholds second. Re-run.
  5. Keep shadow mode — humans confirm every weak — until false-weak is zero on two consecutive sets.

Frequently asked, honestly answered

Errors and recovery

StatusMeaningRecovery
400Malformed payload (missing role/profile, batch over 25, bad Idempotency-Key)Fix the request shape; chunk batches client-side
401Missing or invalid X-API-KeyCheck the key in your dashboard; was it revoked?
404Unknown role idGET /v1/roles lists valid ids
422cv_text under 200 charactersExtraction failed upstream — route that applicant to human review. Never pad the text to pass the gate
429 + Retry-AfterBurst limit (5 req/s per key)Wait the stated seconds; this is not your monthly quota
429, quota messageMonthly screens exhaustedCheck the dashboard usage meter; upgrade or wait for the month to reset
502Judgment model unreachableSafe 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