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.

1. Register โ€” get a session token and an API key
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.
2. Verify a cache hit โ€” use the API key for server-to-server calls
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:

1. A user's question comes in and your cache finds a close-but-not-exact candidate
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 difference
2. Verify before serving it โ€” don't trust the raw similarity score
resp = 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)
3. Once you know the real outcome, feed it back โ€” this is what fine-tuning and drift monitoring learn from
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/login or /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.

ScopeSustained rateBurst
/v1/auth/login, /v1/auth/register10 requests/min5
Everything else under /v1/120 requests/min150

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.

POST
Auth: None
Request body
{
  "email": "you@example.com",
  "password": "at-least-8-chars",
  "tenant_name": "acme"
}
Response 200
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "api_key": "cv_hSDoVrYkFJo2CvoaL1_N9SSQDOWjW6NMSV4DPj9FQ5g",
  "user": { "id": 1, "email": "you@example.com", "tenant_id": 1 }
}
Errors: 400 password too short ยท 409 email already registered

/v1/auth/login

Exchange email + password for a new session token.

POST
Auth: None
Request body
{ "email": "you@example.com", "password": "at-least-8-chars" }
Response 200
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": { "id": 1, "email": "you@example.com", "tenant_id": 1 }
}
Errors: 401 invalid email or password

/v1/auth/me

Look up the currently logged-in user.

GET
Auth: Bearer session token
Response 200
{ "id": 1, "email": "you@example.com", "tenant_id": 1 }
Errors: 401 missing or invalid session

/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.

POST
Auth: Bearer session token only โ€” an API key cannot rotate itself
Response 200
{ "api_key": "cv_hSDoVrYkFJo2CvoaL1_N9SSQDOWjW6NMSV4DPj9FQ5g" }
Errors: 401 missing or invalid session

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.

POST
Auth: X-API-Key or Bearer
Request body
{
  "query": "how do I cancel my subscription",
  "candidate_answer": "Go to Settings > Billing > Cancel."
}
Response 200
{
  "approved": true,
  "score": 1.202,
  "latency_ms": 455.6,
  "model_version": "stock",
  "threshold": 0.0
}
Errors: 401 invalid or missing credentials

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.

POST
Auth: X-API-Key or Bearer
Request body
{
  "items": [
    { "query": "how do I cancel", "candidate_answer": "Settings > Billing > Cancel." },
    { "query": "how do I pay", "candidate_answer": "Settings > Billing > Payment methods." }
  ]
}
Response 200
{
  "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 }
  ]
}
Errors: 422 empty or over 100 items ยท 429 quota can't cover the whole batch โ€” rejected atomically, nothing billed

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.

POST
Auth: X-API-Key or Bearer
Request body
{
  "query": "how do I cancel my subscription",
  "candidate_answer": "Go to Settings > Billing > Cancel.",
  "was_correct": true
}
Response 200
{ "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.

POST
Auth: X-API-Key or Bearer
Request body
{
  "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 }
  ]
}
Response 200
{ "ids": [42, 43] }
Errors: 422 empty or over 500 items

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.

POST
Auth: X-API-Key or Bearer
Response 200
{
  "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
}
Errors: 400 fewer than 20 feedback rows ยท 409 a job is already queued/running for this tenant โ€” poll that one instead

/v1/finetune/jobs

Every fine-tune job this tenant has run, most recent first.

GET
Auth: X-API-Key or Bearer
Response 200
[
  { "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.

GET
Auth: X-API-Key or Bearer
Response 200
{
  "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
}
Errors: 404 job not found

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.

GET
Auth: X-API-Key or Bearer
Response 200
{
  "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 }
}
Errors: 400 tenant has no fine-tuned model yet

Admin

/v1/admin/tenants

Internal tool for ops-assisted tenant provisioning (self-serve users should use /v1/auth/register instead).

POST
Auth: X-Admin-Token
Request body
{ "name": "acme" }
Response 200
{
  "tenant_id": 1,
  "api_key": "cv_hSDoVrYkFJo2CvoaL1_N9SSQDOWjW6NMSV4DPj9FQ5g"
}
Errors: 401 invalid admin token