← Back to Jex

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}
FieldMeaning
labelsrequired 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 / textsone string, or a list (up to the plan's max_batch).
modezero_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 / scoresa softmax over the labels; scores has every label, confidence is the top one's.
second_opinionoptional, 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_progresspresent 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}
NameLabelsTrained onStatus
jex/spamham, spamSMS 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/intent151 (incl. out-of-scope)CLINC150 (CC BY 3.0), full official training split -- 98.28% accuracy / 98.28% macro-F1 on held-out testlive
jex/intent-banking77Banking77 (PolyAI, CC BY 4.0), full official training split -- 94.32% accuracy / 93.85% macro-F1 on held-out testlive
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

ItemPrice
Classify (either mode), per 1,000 texts$0.005
Training, first 10,000 examplesfree
Training, per additional 10,000 examples$0.50
Instant mode's example generationfree -- never billed separately

Errors

StatusWhenBody
400bad or missing fields{"error": "..."} -- a plain, specific message
401missing/unknown key{"error": "missing or unknown API key (Authorization: Bearer jex_...)"}
404no such classifier, or an unknown route{"error": "no such classifier"}
409classifier not ready yet (still training, or failed){"error": "classifier is training", "status": "training"}
413batch or text too large for your plan{"error": "batch too large: 150 > 100"}
429a plan rate limit, or a safety rate/concurrency/daily-spend limit (Retry-After header set){"error": {"message": "...", "type": "rate_limit_exceeded", "tier": "public"}}
501a feature not live yet (ready-made classifiers){"error": "ready-made classifiers are not available yet"}
502instant classify failed internally{"error": "instant classify failed: ..."}
503instant 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