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
- Get a TypeSafe key. Sign up at TypeSafe and copy your API key. Jev runs on your key.
- Send the document text and the values you want checked. Put the key in the
X-Jev-Keyheader. - Read
decisions. Fill the fields markedfill. 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"]}
]
}'
const res = await fetch("https://jevscope.vercel.app/api/v1/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Jev-Key": process.env.TYPESAFE_API_KEY,
},
body: JSON.stringify({
text: documentText,
fields: [
{ name: "invoice_number", value: "INV-2231" },
{ name: "due_date", value: "12 Sep 2026" },
{ name: "currency", options: ["INR", "USD", "EUR"] },
],
}),
});
const { fields, decisions } = await res.json();
decisions.forEach((d, i) => {
if (d.action === "fill") form[d.field] = fields[i].value;
else needsReview.push({ ...d, value: fields[i].value }); // show d.reason to the person
});
import os, requests
res = requests.post(
"https://jevscope.vercel.app/api/v1/verify",
headers={"X-Jev-Key": os.environ["TYPESAFE_API_KEY"]},
json={
"text": document_text,
"fields": [
{"name": "invoice_number", "value": "INV-2231"},
{"name": "due_date", "value": "12 Sep 2026"},
{"name": "currency", "options": ["INR", "USD", "EUR"]},
],
},
timeout=60,
)
res.raise_for_status()
for d in res.json()["decisions"]:
print(d["field"], d["action"], d["confidence"], d["reason"])
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
import os
from jevapi import check_fields, decide_all
items = check_fields(
document_text,
[{"name": "invoice_number", "value": "INV-2231"}], # or None to find every field
key=os.environ["TYPESAFE_API_KEY"],
)
result = decide_all(None, items) # pass a profile once you calibrate
print(result["fill"], result["review"])
How it works
- 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: ...").
- 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.
- 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.
- 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.
| Field | Type | Meaning |
|---|---|---|
text | string, required | The document's text. Read PDFs in your app first. Up to 60,000 characters. |
fields | array, optional | Each 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. |
profile | object, optional | A profile from /calibrate. Without it, scores are Jev's raw numbers. |
options | object, optional | Costs and limits. See Options. |
jevProvider, jevModel | string, optional | Where 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"}]
}'
| Field | Type | Meaning |
|---|---|---|
provider | string, required | openai, anthropic or gemini. |
model | string, required | Your provider's model id. Always your choice. |
text | string, required | The document's text. |
fields | array, optional | {name, description?, type?}. Leave out to ask for every field. |
judge | "jev", optional | Jev's score replaces the AI's own confidence. The AI's number stays in aiConfidence. |
profile, options | optional | Same 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
| Key | Meaning |
|---|---|
action | fill or review. The only thing your app has to act on. |
confidence | The score used for the decision, from 0 to 1. Jev's number, or your calibrated number if you sent a profile. |
lower | A careful lower bound (90%) when calibrated, so a few lucky examples can't unlock auto-fill. |
threshold | The bar for this field, set by your costs. |
reason | One plain sentence you can show to the person checking. |
calibrated, source | Whether a profile was used, and whether it came from this field's own examples (field), all fields pooled (pooled) or none (raw). |
fields[].jev | Jev's own answer: noul (yes/no probability) or choice (picked option), plus certainty. |
Options
| Option | Default | Meaning |
|---|---|---|
costError | 20 | One wrong auto-fill costs as much as this many human checks. Higher means a stricter bar. |
costReview | 1 | Cost of one human check. |
reviewAccuracy | 1 | Share of checks that catch the mistake. |
maxRisk | none | Never auto-fill if the chance of being wrong is above this. |
minExamples | 30 | With fewer examples than this, a field uses the pooled calibration. |
conservative | true | Decide on the 90% lower bound instead of the middle estimate. |
fields | none | Per-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.
| Status | Code | When |
|---|---|---|
| 400 | bad_request, missing_key, missing_jev_key, bad_profile | Something in the request is missing or the wrong shape. |
| 405 | method_not_allowed | Use POST (or GET for /api/v1). |
| 413 | too_large | Body over 1 MB. |
| 429 | rate_limited | Too many requests. Wait for Retry-After seconds. |
| 502 | jev_auth, jev_rate_limited, jev_error, provider errors | TypeSafe or your AI provider said no. Check the key and your quota with them. |
| 504 | jev_timeout | The model took too long. Retry, or send a shorter text. |
Keys and privacy
- Bring your own key. Jev runs on your TypeSafe key, and
/extracton your AI key. You pay them directly. JevAPI never pays for or marks up model calls. - Stateless. The hosted API keeps nothing you send. Keys pass through the server (hosted on Vercel) for one request, and our code never saves or logs them.
- Keep keys on your server. Call JevAPI from your backend, not from a browser, so your key is never shown to users.
- Want full control? Use the package. Then nothing goes through JevAPI's server at all.
Limits (free hosted API)
| calibrate, decide | 60 requests a minute per IP |
| verify, extract | 10 requests a minute per IP |
| Body | 1 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
- Until you calibrate, scores are Jev's raw numbers. They have not been measured on your documents.
- A profile only means something for documents like the ones you labelled.
- The demo's calibration uses synthetic examples.
- JevAPI reads text. Scanned images need OCR first; the demo can read a photo with OCR right in your browser.
MIT licensed. Source on GitHub.