Jex API docs
Everything below is live and real: every request and response shown was captured against the production API. Base URL: https://getjex.dev (or call Railway directly at https://jex-product-production.up.railway.app -- both answer the same service). Auth: Authorization: Bearer jex_... on every endpoint except /health.
Authentication
Every request needs Authorization: Bearer jex_<your key>. A key sees only the classifiers it created. During the preview, admin-minted keys are how we onboard customers directly; self-serve signup lands with public launch.
curl https://getjex.dev/v1/limits -H "Authorization: Bearer $JEX_KEY"
Missing or unknown key -> 401: {"error": "missing or unknown API key (Authorization: Bearer jex_...)"}
Mode 1: instant classify (no training)
POST/v1/classify -- send label names (plus, ideally, a short description of each) and get an answer back immediately, with no training step and nothing to wait for. The very first call on a label set you have never used before answers from label-embedding similarity (mode: "zero_shot_warming") while Jex generates synthetic examples from an open-weight model and fits a small head for that exact label set in the background, a few seconds, once. Every call after that is served from the cached head (mode: "instant_head"), faster and more confident. Common label shapes (spam/ham, sentiment, support-ticket intents, topics) are pre-warmed on every deploy, so they never show the cold-start mode at all.
curl -X POST https://getjex.dev/v1/classify \
-H "Authorization: Bearer $JEX_KEY" -H "Content-Type: application/json" \
-d '{"labels": {"billing": "a payment, invoice, or charge issue",
"bug": "something in the product is broken",
"feature_request": "a request for a new capability"},
"texts": ["I was charged twice this month for one seat"]}'
# first call on this label set:
{"results": [{"label": "billing", "confidence": 0.4092,
"scores": {"billing": 0.4092, "bug": 0.3302, "feature_request": 0.2605}}],
"mode": "zero_shot_warming", "ms": 51.0}
# a later call, same label set, once it has warmed:
{"results": [{"label": "billing", "confidence": 0.804,
"scores": {"billing": 0.804, "bug": 0.1375, "feature_request": 0.0585}}],
"mode": "instant_head", "ms": 11.8}
| Field | Meaning |
|---|---|
labels | required object, 2-200 entries: {name: description}. A description can be "", but 25+ words describing what belongs in the label gives the best cold-start (zero-shot) answer and a better warmed model. |
text / texts | one string, or a list (up to the plan's max_batch). |
mode | zero_shot_warming or instant_head at the top level (see above); a per-result mode: "second_opinion" on any individual text the second opinion corrected (below). |
confidence / scores | a softmax over the labels; scores has every label, confidence is the top one's. |
second_opinion | optional, default true. On by default: any result below 60% confidence gets a second look from the generator (batched, concurrent, capped at 3s -- never blocks longer than that; a slow one just keeps the head's original answer). Measured lift: +7.8pt average accuracy across 12 varied label sets, 114 texts fixed vs 21 broken, p50 latency ~0.7s -> ~3.8s. Send "second_opinion": false to opt out for latency-sensitive calls. |
warm_progress | present only during zero_shot_warming: {"done": n, "total": m} labels generated so far. Large label sets (100+) generate fewer examples per label automatically to keep warm-up bounded. |
No training examples are billed, stored, or shown back to you -- generation happens on our side from an open-weight model only.
Ready-made classifiers
Select a pre-built, pre-trained classifier by name instead of sending your own labels: {"classifier": "jex/spam", "text": "..."}. No warm-up, no generation -- every call is answered from a real head trained once on public, permissively-licensed labelled data. mode is "ready_made".
curl -X POST https://getjex.dev/v1/classify \
-H "Authorization: Bearer $JEX_KEY" -H "Content-Type: application/json" \
-d '{"classifier": "jex/spam", "text": "WIN a free iPhone now! Click here to claim your prize!!!"}'
# {"results": [{"label": "spam", "confidence": 0.965, "confident": true,
# "scores": {"ham": 0.035, "spam": 0.965}}], "mode": "ready_made", "ms": 16.9}
| Name | Labels | Trained on | Status |
|---|---|---|---|
jex/spam | ham, spam | SMS Spam Collection (CC BY 4.0) + 3,823 synthetic spam/ham emails (open-weight model, 5 languages, era-specific styles) -- 98.14% accuracy on its own held-out test. 81.2% accuracy / 66.9% spam recall on a 2,000-message Enron-Spam domain-tuned eval -- not held-out: the synthetic styles were chosen by looking at what we were missing on Enron, so this measures domain match, not generalization to unseen email spam. | live |
jex/intent | 151 (incl. out-of-scope) | CLINC150 (CC BY 3.0), full official training split -- 98.28% accuracy / 98.28% macro-F1 on held-out test | live |
jex/intent-banking | 77 | Banking77 (PolyAI, CC BY 4.0), full official training split -- 94.32% accuracy / 93.85% macro-F1 on held-out test | live |
jex/sentiment, jex/topic | -- | -- | coming |
An unknown name returns 404 with the list of what is available: {"error": "unknown ready-made classifier: '...'; available: [...]"}.
Mode 2: train your own classifier
Send labelled examples once; Jex fits a small head on a shared, frozen, 50+-language encoder and gives you an accuracy report on a held-out split. Use 25 or more examples per label -- fewer makes the report (and the model) unreliable; the API enforces a hard floor of min_examples_per_label (2) but 25+ is what actually works well.
POST/v1/classifiers
curl -X POST https://getjex.dev/v1/classifiers \
-H "Authorization: Bearer $JEX_KEY" -H "Content-Type: application/json" \
-d '{"name": "tickets", "examples": [
{"text": "I was charged twice", "label": "billing"},
{"text": "Export crashes on Safari", "label": "bug"}]}'
# 202 {"id": "c_...", "status": "queued", "queue_position": 0, "n_examples": 2, "duplicates_dropped": 0, "labels": ["billing","bug"]}
Add ?estimate=1 to price a training run without submitting it. Multi-label: send "labels": [...] per example instead of "label". Add "group" (a conversation or document id) so near-duplicate text never lands on both sides of the held-out test split, and optionally "slice" to break the report out by a dimension you care about (e.g. language). "keep_data": true keeps your examples alongside the model until you delete it; by default they are discarded right after training.
GET/v1/classifiers · GET/v1/classifiers/{id}
curl https://getjex.dev/v1/classifiers/c_.../ -H "Authorization: Bearer $JEX_KEY"
# {"id": "c_...", "status": "ready", "labels": [...], "report": {...}, "n_examples": ..., "encoder": "me5:...", ...}
status moves queued -> training -> ready or failed. report (also at /eval) has accuracy and macro-F1 on a held-out, group-safe split, per-label precision/recall, a confusion matrix, and the confidence threshold at which the model is right 90% of the time. /export returns the head's weights once the model is ready.
POST/v1/classifiers/{id}/classify
curl -X POST https://getjex.dev/v1/classifiers/c_.../classify \
-H "Authorization: Bearer $JEX_KEY" -H "Content-Type: application/json" \
-d '{"texts": ["Refund my annual plan"]}'
# {"model": "c_...", "ms": 18.3, "results": [{"label": "billing", "confidence": 0.94, "confident": true, "scores": {...}}]}
Multi-label models return {"labels": [...], "scores": {...}} per item instead of a single label. confident compares against the report's threshold. Boost (optional pooling with a third-party model) is not supported -- a boost field in the request is rejected with 400; Jex never calls a third-party model to answer your classify requests.
DELETE/v1/classifiers/{id}
curl -X DELETE https://getjex.dev/v1/classifiers/c_... -H "Authorization: Bearer $JEX_KEY"
# {"deleted": true, "id": "c_...", "at": 1759..., "files_removed": [...], "gone": true}
GET/v1/limits
Your plan's limits, pricing, this key's usage so far, and its data retention.
curl https://getjex.dev/v1/limits -H "Authorization: Bearer $JEX_KEY"
{"plan": "paid",
"limits": {"max_classifiers": 100, "max_examples": 200000, "max_labels": 100, "min_examples_per_label": 2,
"max_batch": 100, "max_text_chars": 8000, "classify_per_minute": 3000, "trainings_per_day": 200},
"pricing": {"currency": "USD", "classify_per_1k": 0.005, "training_free_examples": 10000, "training_per_10k_examples": 0.5},
"usage": {"classify": 0, "train_examples": 0, "classify_instant": 14},
"retention_days": 90.0}
Rate & spend limits
On top of your plan's per-minute and per-day caps above, every key not on our internal or partner allowlist gets a baseline safety limit: 30 requests/minute, 4 concurrent requests, $5/day (partner keys: 600/min, 64 concurrent, $500/day). A denied request gets 429 (rate, concurrency, or daily-spend) or 503 (a brief service-wide pause) with a Retry-After header in seconds.
HTTP/1.1 429
Retry-After: 37
{"error": {"message": "rate limit: 30 requests per minute", "type": "rate_limit_exceeded", "code": "rate_limit_exceeded", "tier": "public"}}
Ask us to raise your limits once you have real traffic -- that is the partner tier, not a code change.
Under heavy concurrent load, 50-text batch calls queue behind each other on the single serving instance -- measured p50 went from 0.4s (light load) to 3.0s at 50 req/s of batch-50 traffic. Single-text calls stayed fast (p50 ~0.24s wall-clock, ~13ms server time) at every load level we tested. If you expect sustained high-volume batch traffic, send smaller batches or ask us about dedicated capacity.
Pricing
| Item | Price |
|---|---|
| Classify (either mode), per 1,000 texts | $0.005 |
| Training, first 10,000 examples | free |
| Training, per additional 10,000 examples | $0.50 |
| Instant mode's example generation | free -- never billed separately |
Errors
| Status | When | Body |
|---|---|---|
400 | bad or missing fields | {"error": "..."} -- a plain, specific message |
401 | missing/unknown key | {"error": "missing or unknown API key (Authorization: Bearer jex_...)"} |
404 | no such classifier, or an unknown route | {"error": "no such classifier"} |
409 | classifier not ready yet (still training, or failed) | {"error": "classifier is training", "status": "training"} |
413 | batch or text too large for your plan | {"error": "batch too large: 150 > 100"} |
429 | a plan rate limit, or a safety rate/concurrency/daily-spend limit (Retry-After header set) | {"error": {"message": "...", "type": "rate_limit_exceeded", "tier": "public"}} |
501 | a feature not live yet (ready-made classifiers) | {"error": "ready-made classifiers are not available yet"} |
502 | instant classify failed internally | {"error": "instant classify failed: ..."} |
503 | instant classify not configured, or a brief service-wide pause | {"error": "instant classify is not configured on this server"} |
← Back to Jex · How we measured Jex vs Jev · Terms · Privacy · Acceptable Use · support@getjex.dev