JevAPI docs

JevAPI tells your app, field by field, whether a value pulled from a document is safe to fill in automatically or needs a person to check it. Jev, TypeSafe's decision model, reads the document and scores each value. JevAPI turns that score into fill or review, with a reason.

Free and open source (MIT). No sign-up with us. You bring your own keys, so JevAPI costs you nothing to run. Try the demo first if you want to see it work.

Quickstart: first call in 60 seconds

  1. Get a TypeSafe key. Sign up at TypeSafe and copy your API key. Jev runs on your key.
  2. Send the document text and the values you want checked. Put the key in the X-Jev-Key header.
  3. Read decisions. Fill the fields marked fill. Show the rest to a person.
curl -X POST https://jevscope.vercel.app/api/v1/verify \
  -H 'Content-Type: application/json' \
  -H "X-Jev-Key: $TYPESAFE_API_KEY" \
  -d '{
    "text": "ACME SUPPLIES PVT LTD\nInvoice No: INV-2231\nDue Date: 12 Oct 2026\nTotal: Rs 48,200.00",
    "fields": [
      {"name": "invoice_number", "value": "INV-2231"},
      {"name": "due_date", "value": "12 Sep 2026"},
      {"name": "currency", "options": ["INR", "USD", "EUR"]}
    ]
  }'

What came back on a real run (the due date was sent wrong on purpose):

{
  "judge": "jev", "jevModel": "jev-1.13.0",
  "decisions": [
    {"field": "invoice_number", "action": "fill",   "confidence": 0.99, "threshold": 0.95, ...},
    {"field": "due_date",       "action": "review", "confidence": 0.02, "threshold": 0.95,
     "reason": "Not calibrated: using Jev's raw 2%, not yet measured on your documents. Below the 95% bar, so a person should check it."},
    {"field": "currency",       "action": "review", "confidence": 0.85, ...}
  ],
  "fill": 1, "review": 2, "calibrated": false
}

The document said 12 Oct, not 12 Sep, so Jev gave it 2% and it went to review. The currency was never written as "INR", so Jev picked it from "Rs" with 85% and asked for a check.

Use it in your app

There are two ways. Both give the same answers.

Hosted API

Call https://jevscope.vercel.app/api/v1 from any language. Nothing to install. Good for trying it and for small volumes. It has free limits.

Package

Run the same code inside your own app, in JavaScript or Python, with zero dependencies. The maths runs locally. Only the Jev call goes out, straight from your server to TypeSafe.

JavaScript is on npm:

npm i jevapi

Python is on PyPI:

pip install jevapi
import { checkFields, decideAll } from "jevapi";

// One request to Jev for the whole document, using your key.
const { items } = await checkFields({
  document: documentText,
  fields: [{ name: "invoice_number", value: "INV-2231" }], // or null to find every field
  key: process.env.TYPESAFE_API_KEY,
});

const { decisions, fill, review } = decideAll(null, items); // pass a profile once you calibrate

How it works

  1. Get values. From your own extractor, from your AI through /extract, or let JevAPI find them (resumes, receipts, bank statements, forms, and labelled lines like "Invoice No: ...").
  2. Jev checks each one. For every field it answers "does the document really say this?" as a probability, or picks from your list of options. One request per document.
  3. JevAPI decides. Fill when the score clears the bar (95% by default, set by your costs). Everything else goes to review, with a reason a person can read.
  4. Calibrate later. Label a few hundred of your own fields as right or wrong, send them to /calibrate, and the scores are measured against your real documents.

POST /api/v1/verify

Jev checks values against the document text. Header: X-Jev-Key.

FieldTypeMeaning
textstring, requiredThe document's text. Read PDFs in your app first. Up to 60,000 characters.
fieldsarray, optionalEach is {name, value} to check a value, or {name, options:[...]} to let Jev pick (up to 254 options). Add description to say what the field means. Leave out fields and JevAPI finds every field first.
profileobject, optionalA profile from /calibrate. Without it, scores are Jev's raw numbers.
optionsobject, optionalCosts and limits. See Options.
jevProvider, jevModelstring, optionalWhere Jev runs: typesafe (default), venice or openrouter. Model defaults to jev-latest.

Response: judge, discovered, jevModel, usage (tokens), fields (each value with Jev's answer), decisions, fill, review, calibrated and a plain note. See Reading the response.

POST /api/v1/extract

Your own AI reads the document, then JevAPI decides. Header: X-Provider-Key (your OpenAI, Anthropic or Gemini key). Add "judge": "jev" and X-Jev-Key so Jev checks what your AI read. That is the setup we suggest, because AI models often say they are sure when they are not.

curl -X POST https://jevscope.vercel.app/api/v1/extract \
  -H 'Content-Type: application/json' \
  -H "X-Provider-Key: $OPENAI_API_KEY" \
  -H "X-Jev-Key: $TYPESAFE_API_KEY" \
  -d '{
    "provider": "openai",
    "model": "YOUR_MODEL_ID",
    "judge": "jev",
    "text": "INVOICE INV-2231 ...",
    "fields": [{"name": "invoice_number"}, {"name": "total_amount", "type": "number"}]
  }'
FieldTypeMeaning
providerstring, requiredopenai, anthropic or gemini.
modelstring, requiredYour provider's model id. Always your choice.
textstring, requiredThe document's text.
fieldsarray, optional{name, description?, type?}. Leave out to ask for every field.
judge"jev", optionalJev's score replaces the AI's own confidence. The AI's number stays in aiConfidence.
profile, optionsoptionalSame as /verify.

POST /api/v1/decide

No keys, no AI. You already have values and confidences. JevAPI applies the rule and your profile.

curl -X POST https://jevscope.vercel.app/api/v1/decide \
  -H 'Content-Type: application/json' \
  -d '{"items": [
        {"field": "invoice_number", "value": "INV-2231", "confidence": 0.99},
        {"field": "total_amount",   "value": "48200",    "confidence": 0.81}
      ]}'
{"decisions": [
   {"field": "invoice_number", "action": "fill",   "confidence": 0.99, "lower": 0.99, "threshold": 0.95,
    "calibrated": false, "source": "raw", "reason": "Not calibrated: using the extractor's own 99%, ... Clears the 95% bar."},
   {"field": "total_amount",   "action": "review", "confidence": 0.81, "lower": 0.81, "threshold": 0.95,
    "calibrated": false, "source": "raw", "reason": "... Below the 95% bar, so a person should check it."}],
 "fill": 1, "review": 1,
 "note": "No profile sent, so decisions use the raw confidence (not calibrated)."}

POST /api/v1/calibrate

Send fields you checked by hand. You get back a profile (plain JSON). Store it and send it with every call. JevAPI keeps nothing.

curl -X POST https://jevscope.vercel.app/api/v1/calibrate \
  -H 'Content-Type: application/json' \
  -d '{
    "examples": [
      {"field": "total_amount", "confidence": 0.97, "correct": false},
      {"field": "total_amount", "confidence": 0.99, "correct": true}
    ],
    "options": {"costError": 20, "fields": {"total_amount": {"costError": 100}}},
    "evaluate": true
  }'

Response: {profile, evaluation?}. With evaluate: true you also get a held-out (5-fold) check: how much would be filled, how often those fills were wrong, and calibration error before and after. A few hundred examples per important field is a good start. Up to 5,000 per call.

GET /api/v1

Returns the endpoint list, limits and privacy notes as JSON. Handy for a health check.

Recipe: resumes

Send a resume's text to /verify with no fields. JevAPI reads it section by section with no AI key: contact details, links, summary, every job, project and degree, one field per bullet, and skills. Field names look like job_1_title, job_2_company, project_1_name, education_1_degree. When a job lists a product or team beside the title ("Backend Developer   ParcelPoint"), it comes back as job_1_product, and a short line under the title as job_1_about. Each Jev question names the job or project it is about, so two jobs don't get mixed up.

curl -X POST https://jevscope.vercel.app/api/v1/verify \
  -H 'Content-Type: application/json' -H "X-Jev-Key: $TYPESAFE_API_KEY" \
  -d "$(jq -n --rawfile t resume.txt '{text: $t}')"

Group by the prefix (job_1_, project_2_) to show one card per job, like the demo does.

Recipe: receipts, bank statements and forms

With no fields, /verify also reads three more kinds of document with no AI key. A store receipt gives the merchant and date when shown, one field group per item (name, quantity, unit price, price), then every labelled total (subtotal, tax, total, cash, change, and the Indonesian TUNAI / KEMBALI). A bank statement gives the header (bank, account holder, number, IFSC, branch, period, opening and closing balance) and one group per transaction row (date, description, DR/CR, amount, balance). A form gives one field per filled blank ("NAME: ..."), and empty blanks come back with a null value for a person to fill, never invented.

Tested on real public data: 6 receipts from CORD v2 (CC BY 4.0), 3 forms from FUNSD (research use), and 3 bank statements from AgamiAI's Indian-Bank-Statements (Apache 2.0). The bank statement set is synthetic (machine-generated), because real bank statements are not openly licensed for privacy reasons; item lists on the CORD texts cover the item and total area only, so merchant and date are only proposed when the text shows them. Wrapped or split rows stay for review: the reader proposes only what the text plainly says, and Jev checks each value. See spec/receipts.json, spec/bank_statements.json and spec/forms.json for the exact documents, licences and changes.

curl -X POST https://jevscope.vercel.app/api/v1/verify \
  -H 'Content-Type: application/json' -H "X-Jev-Key: $TYPESAFE_API_KEY" \
  -d "$(jq -n --rawfile t receipt.txt '{text: $t}')"

Recipe: invoices

With no fields, /verify finds every labelled line ("Invoice No: ...", "Total: ...") and always asks about the vendor and the currency too, even when they are not labelled. Jev picks the vendor from the letterhead and "payable to" names, and the currency from a list.

{"discovered": true, "fields": [
   {"field": "vendor",   "label": "Vendor",   "value": "ACME SUPPLIES PVT LTD", "confidence": 1},
   {"field": "currency", "label": "Currency", "value": "INR",                   "confidence": 1},
   {"field": "invoice_no", "label": "Invoice No", "value": "INV-2231",         "confidence": 0.99},
   {"field": "total",    "label": "Total",    "value": "Rs 48,200.00",          "confidence": 0.99}, ...],
 "fill": 7, "review": 0}

If you know your form's fields, send them with names that match your database. That is more stable than found labels.

Reading the response

KeyMeaning
actionfill or review. The only thing your app has to act on.
confidenceThe score used for the decision, from 0 to 1. Jev's number, or your calibrated number if you sent a profile.
lowerA careful lower bound (90%) when calibrated, so a few lucky examples can't unlock auto-fill.
thresholdThe bar for this field, set by your costs.
reasonOne plain sentence you can show to the person checking.
calibrated, sourceWhether a profile was used, and whether it came from this field's own examples (field), all fields pooled (pooled) or none (raw).
fields[].jevJev's own answer: noul (yes/no probability) or choice (picked option), plus certainty.

Options

OptionDefaultMeaning
costError20One wrong auto-fill costs as much as this many human checks. Higher means a stricter bar.
costReview1Cost of one human check.
reviewAccuracy1Share of checks that catch the mistake.
maxRisknoneNever auto-fill if the chance of being wrong is above this.
minExamples30With fewer examples than this, a field uses the pooled calibration.
conservativetrueDecide on the 90% lower bound instead of the middle estimate.
fieldsnonePer-field overrides, for example {"total_amount": {"costError": 100}}.

Rule: fill when the score (or its lower bound) is at least reviewAccuracy - costReview / costError, and at least 1 - maxRisk if set. With the defaults that is 95%. Empty values and missing scores always go to review.

Errors

Errors come back as {"error": {"code", "message"}} with a normal HTTP status. The message says what to fix.

StatusCodeWhen
400bad_request, missing_key, missing_jev_key, bad_profileSomething in the request is missing or the wrong shape.
405method_not_allowedUse POST (or GET for /api/v1).
413too_largeBody over 1 MB.
429rate_limitedToo many requests. Wait for Retry-After seconds.
502jev_auth, jev_rate_limited, jev_error, provider errorsTypeSafe or your AI provider said no. Check the key and your quota with them.
504jev_timeoutThe model took too long. Retry, or send a shorter text.

Keys and privacy

Limits (free hosted API)

calibrate, decide60 requests a minute per IP
verify, extract10 requests a minute per IP
Body1 MB, 5,000 examples, 200 items, 50 fields, 60,000 characters of text

Limits are per server and best effort. The service runs on a free hosting plan with monthly caps, so it can pause if it gets very busy. For production traffic, use the package. Every response carries X-RateLimit-Remaining.

Honest limits

MIT licensed. Source on GitHub.