API Reference
All endpoints are under a JSON REST API. Base URL is wherever you deploy this service (e.g. http://localhost:8000 locally).
Quickstart
From zero to a verified call in two requests. Swap in your own deployment's base URL.
curl -X POST https://www.cacheverifier.com/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "you@example.com",
"password": "at-least-8-chars",
"tenant_name": "acme"
}'
# => { "token": "...", "api_key": "cv_...", "user": { ... } }
# Save api_key now -- it's only ever returned this once.curl -X POST https://www.cacheverifier.com/v1/verify \
-H "Content-Type: application/json" \
-H "X-API-Key: cv_..." \
-d '{
"query": "how do I cancel my subscription",
"candidate_answer": "Go to Settings > Billing > Cancel."
}'
# => { "approved": true, "score": 1.202, "latency_ms": 155.0, "model_version": "stock", "threshold": 0.0 }From here: send was_correct ground truth to /v1/feedback as you get it, and once you have a couple hundred rows, kick off a fine-tune job โ or just run the Health Check Report in the dashboard, which does the feedback-upload-and-fine-tune loop for you on a sample.
Integration pattern
CacheVerifier doesn't replace your cache โ you already have one (Redis, GPTCache, a vector store, your own). The problem it solves is the gray zone: a candidate whose similarity score is close enough to look like a hit, but not close enough to trust blindly. Serving it wrong is worse than a cache miss. A worked example โ a support bot backed by a similarity-search cache:
user_query = "how do I cancel my subscription"
candidate = my_cache.search(user_query)
# candidate.answer = "Go to Settings > Billing > Pause subscription for a month."
# candidate.similarity = 0.86 -- close, but "cancel" vs. "pause" is a real differenceresp = requests.post(
"https://www.cacheverifier.com/v1/verify",
headers={"X-API-Key": API_KEY},
json={"query": user_query, "candidate_answer": candidate.answer},
).json()
# => { "approved": false, "score": 0.41, "threshold": 0.5, ... }
if resp["approved"]:
answer = candidate.answer # cache hit confirmed -- skip the LLM call
else:
answer = call_real_llm(user_query) # not trustworthy -- fall through, then re-cache
my_cache.store(user_query, answer)requests.post(
"https://www.cacheverifier.com/v1/feedback",
headers={"X-API-Key": API_KEY},
json={"query": user_query, "candidate_answer": answer, "was_correct": True},
)From there, either call POST /v1/finetune/jobs yourself once feedback builds up (self-serve, free forever), or subscribe to a Managed plan and let it happen on a schedule automatically, with drift alerts if the gray zone starts behaving differently than it did at training time.
Already on GPTCache? There's a drop-in SimilarityEvaluation adapter that wraps this exact call โ pass it as GPTCache's similarity_evaluation= argument and the if/else above happens inside GPTCache's own pipeline, no extra code required.
Authentication
Tenant-scoped endpoints (verify, feedback, fine-tune, drift) accept either credential โ whichever is more convenient for the caller:
X-API-Key: cv_...โ a raw tenant API key, for server-to-server integrations. Issued once at registration or by an admin; only the hash is stored, so it can't be recovered if lost.Authorization: Bearer <token>โ a dashboard session token from/v1/auth/loginor/v1/auth/register, valid for 24 hours.
X-Admin-Token is separate and only guards POST /v1/admin/tenants (internal tenant provisioning).
Rate limits
Enforced at the edge, keyed by X-API-Key when present (so multiple tenants behind the same NAT/corporate IP don't share a bucket), falling back to client IP for unauthenticated calls and dashboard session-token requests. Exceeding a limit returns a plain 429 with no body โ back off and retry.
| Scope | Sustained rate | Burst |
|---|---|---|
/v1/auth/login, /v1/auth/register | 10 requests/min | 5 |
Everything else under /v1/ | 120 requests/min | 150 |
The general limit's burst allowance is sized for bulk flows like the Health Check Report, which submits many /v1/feedback rows back-to-back โ a single run of a hundred or so rows won't get throttled. The auth limit is intentionally tight: it's there to slow down credential stuffing and mass signup, not to accommodate normal traffic.
Auth
/v1/auth/register
Create a tenant, an API key, and a dashboard user in one call.
{
"email": "you@example.com",
"password": "at-least-8-chars",
"tenant_name": "acme"
}{
"token": "eyJhbGciOiJIUzI1NiIs...",
"api_key": "cv_hSDoVrYkFJo2CvoaL1_N9SSQDOWjW6NMSV4DPj9FQ5g",
"user": { "id": 1, "email": "you@example.com", "tenant_id": 1 }
}/v1/auth/login
Exchange email + password for a new session token.
{ "email": "you@example.com", "password": "at-least-8-chars" }{
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": 1, "email": "you@example.com", "tenant_id": 1 }
}/v1/auth/me
Look up the currently logged-in user.
{ "id": 1, "email": "you@example.com", "tenant_id": 1 }/v1/auth/rotate-key
Revoke every currently-active API key for this tenant and issue a fresh one. Do this if a key ever leaks.
{ "api_key": "cv_hSDoVrYkFJo2CvoaL1_N9SSQDOWjW6NMSV4DPj9FQ5g" }rotate-key requires a dashboard login (Authorization: Bearer), not the API key itself โ otherwise a leaked key could keep itself alive. Update any server-to-server integration with the new key immediately; the old one stops working the instant this call succeeds.
Verify
/v1/verify
Synchronous gray-zone verification: approve or reject serving a candidate cached answer.
{
"query": "how do I cancel my subscription",
"candidate_answer": "Go to Settings > Billing > Cancel."
}{
"approved": true,
"score": 1.202,
"latency_ms": 455.6,
"model_version": "stock",
"threshold": 0.0
}threshold is the score >= threshold cutoff this call was decided against โ0.0 on the shared stock model, tenant-specific once a fine-tune job has calibrated one (see Fine-tune Jobs below).
/v1/verify/batch
Same as /v1/verify for up to 100 pairs in one request โ one HTTP round trip, and one batched model forward pass for the stock cross-encoder.
{
"items": [
{ "query": "how do I cancel", "candidate_answer": "Settings > Billing > Cancel." },
{ "query": "how do I pay", "candidate_answer": "Settings > Billing > Payment methods." }
]
}{
"results": [
{ "approved": true, "score": 1.202, "latency_ms": 61.4, "model_version": "stock", "threshold": 0.0 },
{ "approved": true, "score": 0.98, "latency_ms": 61.4, "model_version": "stock", "threshold": 0.0 }
]
}The whole batch is quota-checked and billed as a single unit: admitted only if capacity covers every item, rejected entirely otherwise โ never partially processed. latency_ms on a batch response is the whole batch's time, not a true per-item measurement (there's no way to isolate one pair's share of a single forward pass).
Feedback
/v1/feedback
Record ground truth for a past verify/candidate pair โ trains fine-tuning and drift monitoring.
{
"query": "how do I cancel my subscription",
"candidate_answer": "Go to Settings > Billing > Cancel.",
"was_correct": true
}{ "id": 42 }Pass an Idempotency-Key header to make retries safe โ retrying the same key returns the original row's id instead of creating a duplicate label. Omit it and the same (query, candidate_answer) pair is free to get independent feedback rows, which is the common case (different users hitting the same cached answer).
/v1/feedback/batch
Same as /v1/feedback for up to 500 rows in one request โ for bulk uploads like the Health Check Report.
{
"items": [
{ "query": "how do I cancel", "candidate_answer": "Settings > Billing > Cancel.", "was_correct": true },
{ "query": "how do I cancel", "candidate_answer": "Settings > Billing > Pause.", "was_correct": false }
]
}{ "ids": [42, 43] }Fine-tune Jobs
/v1/finetune/jobs
Fine-tune the tenant's verifier on every feedback row submitted so far. Runs async on an RQ queue.
{
"id": 7,
"status": "queued",
"n_examples": 214,
"warning": null,
"result_model_version": null,
"auc_baseline": null,
"auc_tuned": null,
"test_n": null,
"threshold": null,
"threshold_calibrated": null,
"error": null
}/v1/finetune/jobs
Every fine-tune job this tenant has run, most recent first.
[
{ "id": 7, "status": "done", "n_examples": 214, "result_model_version": 3, "auc_baseline": 0.71, "auc_tuned": 0.86, "test_n": 43, "threshold": 0.184, "threshold_calibrated": true, "warning": null, "error": null }
]/v1/finetune/jobs/{id}
Poll job status. status is one of queued, running, done, failed.
{
"id": 7,
"status": "done",
"n_examples": 214,
"warning": null,
"result_model_version": 3,
"auc_baseline": 0.71,
"auc_tuned": 0.86,
"test_n": 43,
"threshold": 0.184,
"threshold_calibrated": true,
"error": null
}threshold is picked from this job's own held-out scores (Youden's J statistic) once at least 10 held-out examples with both labels present are available, and becomes this tenant's new /v1/verify cutoff immediately. threshold_calibrated: false means the held-out slice was too small โ the tenant's previous threshold (or 0.0, before any successful calibration) stays in effect instead of an untrustworthy new one.
Drift Monitor
/v1/monitor/drift-status
Checks feedback collected since the active model went live against its training-window positive rate.
{
"status": "stable",
"n_labels": 96,
"baseline_positive_rate": 0.31,
"chunked_z_test": {
"baseline_rate": 0.31,
"n_baseline": 214,
"chunks": [ { "chunk": 0, "n": 12, "positive_rate": 0.33, "z": 0.2, "p_value": 0.84, "flagged": false } ],
"first_flagged_chunk": null
},
"page_hinkley": { "flagged": false, "flag_index": null, "final_statistic": 0.04 }
}Admin
/v1/admin/tenants
Internal tool for ops-assisted tenant provisioning (self-serve users should use /v1/auth/register instead).
{ "name": "acme" }{
"tenant_id": 1,
"api_key": "cv_hSDoVrYkFJo2CvoaL1_N9SSQDOWjW6NMSV4DPj9FQ5g"
}