Loading…
Loading…
PaperTrail exposes a small JSON HTTP API. All responses are JSON. Errors return a { "error": string } body with an appropriate HTTP status. Replace your-deployment.vercel.app with your host (or use a relative path from the same origin).
POST /api/verify* endpoints are rate-limited per IP. When the server runs with DEMO_MODE=true, retrieval reads only from the pre-cached sources table and never live-fetches PubMed or ClinicalTrials.gov — so claims outside the cached demo set return no_support_found./api/verifyThe core endpoint. Retrieves the best-matching primary source, extracts its finding, and returns a grounded verification whose flagged spans map to exact char offsets in source.raw_text. Rate-limited per client IP. Persistence is best-effort: a DB failure returns the result with verification_id: null./api/verify/batchRuns the single-claim pipeline over multiple claims sequentially, capped at 5 per request to bound token spend. Each claim is isolated: one failure yields status: "error" for that item without sinking the batch./api/verify/textBring-your-own-source: verifies a claim against arbitrary pasted text. No retrieval, no DB read/write, so no permalink is minted (verification_id is omitted). Flagged spans are grounded against the pasted text./api/verificationsPaginated, newest-first list of stored verifications, optionally filtered by discrepancy_type. Returns items plus total for pagination./api/verifications/[id]LLM-free shareable permalink for one stored verification. Stored flagged spans are re-grounded against the current cached source text so offsets stay valid. If the source was removed, source is null, flagged_spans is empty, and effect_size_check is null./api/sourcesPaginated list of the cached primary sources PaperTrail can verify against, optionally filtered by a title/external_id query. Full source text omitted from the list./api/sources/[id]One cached source with its full text plus the verifications that matched against it./api/statsAggregate counts across cached sources and stored verifications, for a dashboard or health-at-a-glance view./api/healthLiveness and dependency check. 200 / status "ok" when the database is reachable and required API keys are present; otherwise 503 / status "degraded"./api/verifyThe core endpoint. Retrieves the best-matching primary source, extracts its finding, and returns a grounded verification whose flagged spans map to exact char offsets in source.raw_text. Rate-limited per client IP. Persistence is best-effort: a DB failure returns the result with verification_id: null.
Example (curl)
curl -X POST https://your-deployment.vercel.app/api/verify \
-H "Content-Type: application/json" \
-d '{"claim": "Drug X reduced major cardiac events by 30% in adults with heart failure."}'Request
{ "claim": string, "source_hint"?: string }
// claim: 10–2000 chars after trimming
// source_hint (optional): a DOI / PMID / NCT you actually cited, to pin retrievalResponse
// 200 — verified
{
"status": "verified",
"verification_id": string | null, // null if the DB write failed (result still returned)
"claim": string,
"source": {
"title": string | null,
"url": string,
"source_type": string, // "pubmed" | "clinicaltrials"
"external_id": string,
"raw_text": string // full cached source text
},
"finding": { ... }, // structured extraction
"verification": {
"discrepancy_type": string, // e.g. "accurate" | "magnitude_overstated" | ...
"trust_score": number, // 0–100
"explanation": string,
"flagged_spans": [ { "text": string, "grounding": { "start": number, "end": number } } ],
"grounding_dropped_count": number
},
"effect_size_check": { "verdict": string, ... }
}
// 200 — no confident source
{ "status": "no_support_found", "message": string }
// 400 invalid JSON / claim length · 429 rate limited · 500 pipeline error
{ "error": string }/api/verify/batchRuns the single-claim pipeline over multiple claims sequentially, capped at 5 per request to bound token spend. Each claim is isolated: one failure yields status: "error" for that item without sinking the batch.
Request
{ "claims"?: string[], "text"?: string }
// claims[] wins if present; otherwise "text" is split into claims.
// At most the first 5 claims are processed (hard cap), sequentially.Response
// 200
{
"results": [
{
"claim": string,
"status": "verified" | "no_support_found" | "error",
"verification_id"?: string | null,
"source"?: { "title": string | null, "url": string, "source_type": string, "external_id": string, "raw_text": string },
"verification"?: { "discrepancy_type": string, "trust_score": number, "explanation": string, "flagged_spans": [...], "grounding_dropped_count": number },
"effect_size_check"?: { "verdict": string, ... }
}
],
"truncated": boolean, // true if more than 5 claims were detected
"total_detected": number
}
// 400 invalid JSON / no claims detected · 429 rate limited
{ "error": string }/api/verify/textBring-your-own-source: verifies a claim against arbitrary pasted text. No retrieval, no DB read/write, so no permalink is minted (verification_id is omitted). Flagged spans are grounded against the pasted text.
Request
{ "claim": string, "source_text": string }
// claim: >= 10 chars · source_text: 40–20000 chars, both after trimmingResponse
// 200
{
"status": "verified",
"claim": string,
"source": { "title": "Pasted source", "url": "", "source_type": "custom", "raw_text": string },
"finding": { ... },
"verification": { "discrepancy_type": string, "trust_score": number, "explanation": string, "flagged_spans": [...], "grounding_dropped_count": number },
"effect_size_check": { "verdict": string, ... }
}
// 400 invalid JSON / claim or source_text length · 429 rate limited · 500 error
{ "error": string }/api/verificationsPaginated, newest-first list of stored verifications, optionally filtered by discrepancy_type. Returns items plus total for pagination.
Request
// optional: ?limit= (1..100, default 20) &offset= (>=0) &discrepancy_type=
curl "/api/verifications?limit=20&offset=0&discrepancy_type=magnitude_overstated"Response
// 200
{
"items": [
{
"id": string, // uuid
"claim_text": string,
"discrepancy_type": string,
"trust_score": number,
"created_at": string // ISO timestamp
}
],
"total": number // total matching rows (for pagination)
}
// 500 { "error": string }/api/verifications/[id]LLM-free shareable permalink for one stored verification. Stored flagged spans are re-grounded against the current cached source text so offsets stay valid. If the source was removed, source is null, flagged_spans is empty, and effect_size_check is null.
Request
// [id] must be a valid UUID
curl /api/verifications/3f9c…-uuidResponse
// 200 — mirrors POST /api/verify success shape, plus created_at.
{
"status": "verified",
"verification_id": string,
"claim": string,
"created_at": string,
"source": { "title": string | null, "url": string | null, "source_type": string | null, "external_id": string | null, "raw_text": string } | null,
"verification": { "discrepancy_type": string, "trust_score": number, "explanation": string, "flagged_spans": [...] },
"effect_size_check": { "verdict": string, ... } | null
}
// 400 id not a valid UUID · 404 not found · 500 error
{ "error": string }/api/sourcesPaginated list of the cached primary sources PaperTrail can verify against, optionally filtered by a title/external_id query. Full source text omitted from the list.
Request
// optional: ?limit= (1..100, default 50) &offset= (>=0) &q= (title/external_id search)
curl "/api/sources?limit=50&offset=0&q=lecanemab"Response
// 200
{
"items": [
{
"id": string, // uuid
"source_type": string, // "pubmed" | "clinicaltrials"
"external_id": string,
"title": string | null,
"url": string
}
],
"total": number // total matching sources (for pagination)
}
// 500 { "error": string }/api/sources/[id]One cached source with its full text plus the verifications that matched against it.
Request
// [id] must be a valid UUID
curl /api/sources/a1b2…-uuidResponse
// 200
{
"source": {
"id": string,
"source_type": string,
"external_id": string,
"title": string | null,
"url": string,
"raw_text": string // full cached source text
},
"verifications": [ // up to 50 verifications matched to this source, newest first
{ "id": string, "claim_text": string, "discrepancy_type": string, "trust_score": number, "created_at": string }
]
}
// 400 id not a valid UUID · 404 not found · 500 error
{ "error": string }/api/statsAggregate counts across cached sources and stored verifications, for a dashboard or health-at-a-glance view.
Request
curl /api/statsResponse
// 200
{
"total_verifications": number,
"total_sources": number,
"avg_trust_score": number | null, // rounded, null if no verifications
"by_discrepancy_type": { [type: string]: number },
"flagged_rate": number // share of verifications with a non-"accurate" type (0–1)
}
// 500 { "error": string }/api/healthLiveness and dependency check. 200 / status "ok" when the database is reachable and required API keys are present; otherwise 503 / status "degraded".
Request
curl /api/healthResponse
// 200 (ok) or 503 (degraded)
{
"status": "ok" | "degraded",
"checks": {
"database": boolean,
"anthropic_key_present": boolean,
"voyage_key_present": boolean
},
"timestamp": string // ISO timestamp
}