Frontend integration guide: stable legacy routes, additive document scan and citation verification,
REST payloads, WebSockets, and secret_key authentication. Relative API prefix:
/api/v1.
This section is the handoff summary for the frontend team. It states exactly what existed before, what this release adds, and what must remain untouched during migration.
| Existing capability | Existing endpoint | Decision |
|---|---|---|
| Passage AI detection | POST /api/v1/scan/ai-scan | KEEP AS-IS No request or response change. |
| Sentence/deep AI detection | POST /api/v1/scan/deep-scan | KEEP AS-IS No request or response change. |
| Document text extraction | POST /api/v1/scan/extract-document-text | KEEP AS-IS Still used when the frontend only needs extracted text. |
| Document metadata forensics | POST /api/v1/scan/document-forensics | KEEP AS-IS This remains a separate metadata/editing analysis. |
| Streaming text explanation | WS /api/v1/explanation/ws/explain-text | KEEP AS-IS No WebSocket payload change. |
Important distinction: before this release, this standalone text
service did not expose the improved Document authenticity engine or Citation verification.
document-forensics is not being renamed or removed; it remains its own specialized endpoint.
| New capability | New endpoint | Frontend change |
|---|---|---|
| Document scan — improved authenticity, provenance, OCR, forensic and text-model evidence | POST /api/v1/scan/document-scanGET /api/v1/scan/document-scan/jobs/{job_id} | Add a new asynchronous single-file flow for PDF, DOCX, and document images. Do not replace an existing call in-place. |
| Document scan batch | POST /api/v1/scan/document-scan/batch | Additive multi-file endpoint. It returns one normal job per file, polled through the existing job endpoint. |
| Document authenticity old product name | POST /api/v1/scan/document-authenticity | Compatibility alias only. New frontend code should display “Document scan” and call the preferred endpoint. |
| Citation verification from text | POST /api/v1/citation/verifyGET /api/v1/citation/jobs/{job_id} | Add as an independent asynchronous feature. |
| Citation verification from file | POST /api/v1/citation/verify-document | Optional PDF/DOCX/TXT/Markdown upload entry point; poll the same citation job endpoint. |
Frontend starting point: leave the current API client untouched. Create a separate
documentScan client and a separate citationVerification client using the shared
asynchronous polling helper in No-downtime frontend migration.
Every REST request must include secret_key matching the server
SECRET_KEY in .env. Invalid or missing key: 403.
secret_key in the body alongside text.secret_key as a form field with file.X-API-Key: <secret_key>. A deprecated
?secret_key= fallback is accepted for migration only.WebSockets: include secret_key in the first JSON frame.
| Method | Path | Purpose |
|---|---|---|
| GET | / | This integration page (HTML) |
| GET | /api/v1/discovery.json | Discovery JSON (machine-readable index) |
| POST | /api/v1/scan/ai-scan | Passage-level Human / AI / Uncertain — scores, radar_axes[], optional source_attribution |
| POST | /api/v1/scan/deep-scan | Sentence-level windows — dual verdicts, radar_axes[] (passage-aligned), scores, labels |
| POST | /api/v1/scan/extract-document-text | Extract plain text from PDF / DOCX / txt |
| POST | /api/v1/scan/document-forensics | Document metadata forensics (PDF / DOCX) |
| POST | /api/v1/scan/document-scan | Queue one improved document scan (PDF, DOCX, or document image) |
| POST | /api/v1/scan/document-scan/batch | Queue 1–10 files as independent document-scan jobs (50 MB combined) |
| GET | /api/v1/scan/document-scan/jobs/{job_id} | Poll document scan until done or error |
| POST | /api/v1/citation/verify | Queue legal/academic citation verification from text |
| POST | /api/v1/citation/verify-document | Queue citation verification from PDF / DOCX / text document |
| GET | /api/v1/citation/jobs/{job_id} | Poll citation verification result |
| WS | /api/v1/explanation/ws/explain-text | Streaming forensic text explanation |
| GET | /docs | Swagger UI |
| GET | /api/v1/openapi.json | OpenAPI JSON |
Compatibility: the four existing scan paths and their request/response bodies are unchanged. New clients opt into the new paths; old clients continue operating during rollout.
Polar charts: POST …/ai-scan and POST …/deep-scan both return radar_axes[] — five heuristic dimensions (integer 0–100 each) keyed for stable UI bindings. Derived from submitted text, passage ai_score, and the verdict probability inferred on the server (same basis as Dashboard confidenceValue). Axis semantics and a reference table → Radar axes.
/api/v1/scan/ai-scan — Full-passage classification: Human, AI, or Uncertain. Legacy field
confidence is passage-level P(AI) (same numeric value as ai_score).
Response includes radar_axes[] for polar charts (passage-aligned heuristics). Optional
source_attribution when the passage is flagged AI (vendor hints: OpenAI, Gemini, Claude).
{
"secret_key": "<server SECRET_KEY from .env>",
"text": "Your passage …",
"config": null
}
SECRET_KEY.ParameterConfig (threshold, batch_size, context_sentences, …).{
"prediction": "AI",
"confidence": 0.71,
"is_mixed": false,
"is_ai": true,
"ai_score": 0.71,
"mixed_score": 0.0,
"human_score": 0.29,
"radar_axes": [
{"key": "ai_signal", "label": "AI Signal", "value": 71},
{"key": "uniformity", "label": "Uniformity", "value": 62},
{"key": "confidence", "label": "Confidence", "value": 71},
{"key": "perplexity", "label": "Perplexity", "value": 54},
{"key": "burstiness", "label": "Burstiness", "value": 58}
],
"source_attribution": {
"status": "ok",
"likely_source": "openai",
"likely_label": "OpenAI",
"confidence": 0.42,
"margin": 0.11,
"candidates": [
{"source": "openai", "label": "OpenAI", "score": 0.42},
{"source": "gemini", "label": "Google Gemini", "score": 0.28}
],
"source_display": {
"kind": "single",
"options": [{"source": "openai", "label": "OpenAI · ChatGPT"}],
"show_confidence": true
},
"evidence": ["HF RoBERTa ChatGPT-detector affinity ≈ 62%"],
"method": "hf_chatgpt_roberta+lmscan",
"word_count": 312,
"detail": null
}
}
0 on AI-only passage scan.key, label, integer value); see Radar axes./api/v1/scan/deep-scan — Sentence windows with neighbor context; each row exposes
prediction, ai_probability (P(AI)), and optional model fields when enabled.
Passage-level P(AI) appears as ai_score (alongside human_score).
Dual verdicts: passage_verdict + distribution_verdict; distribution_confidence tiers the sentence-mix label.
radar_axes[] mirrors the passage headline (same inputs as AI scan)—not recomputed from raw sentence-frequency counts alone.
{
"secret_key": "<server SECRET_KEY from .env>",
"text": "Your document …",
"config": null
}
{
"sentences": [
{
"sentence": "…",
"prediction": "AI",
"ai_probability": 0.94
}
],
"distribution_confidence": 0.72,
"passage_verdict": "AI",
"distribution_verdict": "AIWrittenHumanModified",
"deep_labels": ["AI", "AIWrittenHumanModified"],
"overall_prediction": "AI",
"is_mixed": false,
"mixed_ratio": 0.45,
"ai_score": 0.71,
"mixed_score": 0.12,
"uncertain_sentence_ratio": 0.12,
"human_score": 0.29,
"radar_axes": [
{"key": "ai_signal", "label": "AI Signal", "value": 71},
{"key": "uniformity", "label": "Uniformity", "value": 62},
{"key": "confidence", "label": "Confidence", "value": 68},
{"key": "perplexity", "label": "Perplexity", "value": 54},
{"key": "burstiness", "label": "Burstiness", "value": 58}
],
"source_attribution": { }
}
[passage_verdict, distribution_verdict].Multipart: secret_key (form) + file — PDF, DOCX, txt, md. Returns text, word_count, filename.
Multipart: secret_key (form) + file — Metadata forensics (AI-tool fingerprints, editing anomalies, timestamps, fonts). No neural text classifier.
{
"doc_type": "docx",
"filename": "report.docx",
"suspicion_score": 0.34,
"verdict": "Inconclusive",
"signals": [ ],
"editing_behaviour": { },
"summary": "…"
}
Preferred new endpoint. Multipart secret_key + file;
PDF, DOCX, PNG, JPEG, WebP, or TIFF. Returns HTTP 202 immediately. The former product name remains available
as deprecated alias /api/v1/scan/document-authenticity with the same request and response.
{
"job_id": "b7c2…",
"kind": "document_scan",
"filename": "invoice.pdf",
"status": "queued",
"stage_message": "Queued",
"result": null,
"error": null,
"created_at": 1786300000.1,
"updated_at": 1786300000.1
}
GET /api/v1/scan/document-scan/jobs/{job_id}
X-API-Key: <server SECRET_KEY>
When status is done, result contains:
outcome, requires_manual_review, risk_score,
confidence_status, limitations[], evidence[],
decision_basis, provenance, metadata/page hashes, OCR summary,
visual analysis, and text-model evidence. Treat verified as reserved for trusted
cryptographic proof; no_decisive_manipulation_evidence is not issuer verification.
New additive batch endpoint. Multipart
secret_key plus the repeated form field files. Supports the same
PDF, DOCX, PNG, JPEG, WebP, and TIFF formats. Maximum 10 files and 50 MB total per batch.
{
"batch_id": "8f09…",
"status": "queued",
"total_files": 2,
"jobs": [
{
"job_id": "a1…",
"kind": "document_scan",
"filename": "contract.docx",
"status": "queued",
"stage_message": "Queued",
"result": null,
"error": null,
"created_at": 1786300000.1,
"updated_at": 1786300000.1
}
],
"poll_pattern": "/api/v1/scan/document-scan/jobs/{job_id}"
}
There is deliberately no new batch-result shape: poll every item in
jobs[] through the existing single-job endpoint. Each completed
result has the same Document scan response used by single-file uploads.
{
"secret_key": "<server SECRET_KEY>",
"text": "Document text containing citations …",
"domain": "auto",
"jurisdiction_preference": "both",
"enable_context_check": true,
"use_gemini": true,
"max_citations": 200
}
Returns the same 202 job envelope. Poll /api/v1/citation/jobs/{job_id} with
X-API-Key. On completion, result contains session_id,
extracted tokens[], verified items[], and summary.
Item status is green, yellow, or red;
render the accompanying classification and evidence[], not color alone.
Multipart alternative: secret_key, file, and optional form fields
domain, jurisdiction_preference, enable_context_check,
use_gemini, max_citations. Supports PDF, DOCX, TXT, and Markdown,
with a maximum file size of 50 MB. Nginx allows multipart overhead separately,
while the application enforces the 50 MB file limit.
After a completed job, download the optional report from
/api/v1/citation/report/{session_id} using the returned result.session_id.
What changed: the improved Orivfy Document authenticity engine is now presented by this service as Document scan. Citation verification is a separate new module. Both use asynchronous jobs so uploads and provider checks do not hold one HTTP request open. This is an additive release: no existing frontend call needs to be renamed or rewritten.
| Integration | Status | Frontend action |
|---|---|---|
POST /api/v1/scan/ai-scan | UNCHANGED | Keep the existing request and response handling. |
POST /api/v1/scan/deep-scan | UNCHANGED | Keep the existing request and response handling. |
POST /api/v1/scan/extract-document-text | UNCHANGED | Keep as the text-extraction utility. |
POST /api/v1/scan/document-forensics | UNCHANGED | Keep as metadata/editing forensics; it has not been replaced. |
WS /api/v1/explanation/ws/explain-text | UNCHANGED | Keep the current explanation WebSocket integration. |
POST /api/v1/scan/document-scan | NEW | Add the asynchronous document-scan flow described below. |
POST /api/v1/scan/document-scan/batch | NEW | Use only for multi-file uploads; single-file clients remain unchanged. |
POST /api/v1/scan/document-authenticity | COMPATIBILITY ALIAS | Do not use for new screens; retained while old naming is retired. |
POST /api/v1/citation/verify | NEW | Add independently from document scan. |
202 and save job_id.X-API-Key.queued or processing, display stage_message.done, read the module payload from result.error, stop polling and display error with a retry action.job_id so the user can retry status later.type AnalysisJob<T> = {
job_id: string;
kind: "document_scan" | "citation_verification";
filename: string;
status: "queued" | "processing" | "done" | "error";
stage_message: string;
result: T | null;
error: string | null;
created_at: number;
updated_at: number;
};
async function pollJob<T>(
jobPath: string,
apiKey: string,
signal?: AbortSignal,
): Promise<AnalysisJob<T>> {
for (;;) {
const response = await fetch(jobPath, {
headers: { "X-API-Key": apiKey },
cache: "no-store",
signal,
});
if (!response.ok) throw new Error(`Job poll failed: ${response.status}`);
const job = (await response.json()) as AnalysisJob<T>;
if (job.status === "done") return job;
if (job.status === "error") throw new Error(job.error || "Analysis failed");
await new Promise(resolve => setTimeout(resolve, 1500));
}
}
const form = new FormData();
form.append("secret_key", apiKey);
form.append("file", selectedFile);
const createdResponse = await fetch("/api/v1/scan/document-scan", {
method: "POST",
body: form,
cache: "no-store",
});
if (createdResponse.status !== 202) {
throw new Error(`Document scan was not queued: ${createdResponse.status}`);
}
const created = await createdResponse.json();
const completed = await pollJob(
"/api/v1/scan/document-scan/jobs/{job_id}".replace("{job_id}", created.job_id),
apiKey,
);
const documentResult = completed.result;
const form = new FormData();
form.append("secret_key", apiKey);
for (const file of selectedFiles) form.append("files", file);
const createdResponse = await fetch("/api/v1/scan/document-scan/batch", {
method: "POST",
body: form,
cache: "no-store",
});
if (createdResponse.status !== 202) {
throw new Error(`Document batch was not queued: ${createdResponse.status}`);
}
const batch = await createdResponse.json();
const completedJobs = await Promise.all(
batch.jobs.map(job =>
pollJob(
batch.poll_pattern.replace("{job_id}", job.job_id),
apiKey,
),
),
);
const documentResults = completedJobs.map(job => job.result);
Keep each file’s job_id, status, error, and result separate so one failed
file does not hide successful files in the same user-selected batch.
Recommended UI mapping for documentResult.outcome:
| Outcome | Suggested UI label | Important behavior |
|---|---|---|
verified | Verified proof | Only display as verified when the response has trusted cryptographic/issuer proof. |
likely_manipulated | Likely manipulated | Show evidence and require manual review. |
likely_ai_generated | Likely AI-generated | This concerns content origin; it is not by itself proof of issuer fraud. |
suspicious_automated_generation | Suspicious automated generation | Show supporting metadata and require review. |
no_decisive_manipulation_evidence | No decisive manipulation evidence | Never shorten this to “Verified” or “Genuine.” |
manual_review | Manual review required | Show decision_basis.reason and evidence[]. |
requires_manual_review as the workflow control; do not infer it from colors.risk_score as a risk indicator, not calibrated probability or accuracy.limitations[] and allow model_evidence.status or optional tools to be unavailable.category, code, severity, description); ignore unknown future fields.const createdResponse = await fetch("/api/v1/citation/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret_key: apiKey,
text: documentText,
domain: "auto",
jurisdiction_preference: "both",
enable_context_check: true,
use_gemini: true,
max_citations: 200,
}),
cache: "no-store",
});
if (createdResponse.status !== 202) {
throw new Error(`Citation verification was not queued: ${createdResponse.status}`);
}
const created = await createdResponse.json();
const completed = await pollJob(
"/api/v1/citation/jobs/{job_id}".replace("{job_id}", created.job_id),
apiKey,
);
const { session_id, summary, items } = completed.result;
total_citations, green_count, yellow_count, red_count, compliance_index.citation_text, status, classification, existence_verified, and evidence links.green = verified; yellow = mismatch/typo/review; red = not verified. Always include text labels for accessibility.{session_id} in /api/v1/citation/report/{session_id}.Secret handling: the current API contract accepts secret_key
because that is how the already-integrated text service authenticates. Prefer calling this service through
your own server-side frontend/API proxy; do not compile a production server secret into public browser JavaScript.
Open WebSocket → send one JSON text frame. Use wss: when API is HTTPS.
POST …/deep-scan){
"secret_key": "…",
"prediction": "AI",
"overall_prediction": "AI",
"passage_verdict": "AI",
"distribution_verdict": "AIWrittenHumanModified",
"confidence": 0.71,
"distribution_confidence": 0.72,
"text": "",
"ai_score": 0.71,
"human_score": 0.29,
"mixed_score": 0.12,
"radar_axes": [
{"key": "ai_signal", "label": "AI Signal", "value": 71},
{"key": "uniformity", "label": "Uniformity", "value": 62},
{"key": "confidence", "label": "Confidence", "value": 68},
{"key": "perplexity", "label": "Perplexity", "value": 54},
{"key": "burstiness", "label": "Burstiness", "value": 58}
],
"sentences": [
{
"sentence": "…",
"prediction": "Human",
"ai_probability": 0.19
}
]
}
confidence (same number as scan ai_score); include distribution_prediction when using deep-scan mode.POST …/deep-scan (same shape as REST), including radar_axes — the WS handler ignores facets it does not need.content, partial_textfinal_textmessage| Field | Meaning |
|---|---|
prediction / passage_verdict | Human · AI · Uncertain (passage-level) |
distribution_verdict | Sentence-mix label (see table below) |
deep_labels | [passage_verdict, distribution_verdict] |
is_ai | True when passage is AI or Uncertain (AI scan only) |
distribution_confidence | Blended 0–1 score for distribution verdict (deep scan) |
confidence (AI scan) | Passage P(AI); same numeric value as ai_score |
ai_score | Passage P(AI) (recommended for new clients) |
mixed_ratio | Fraction of sentences classified AI |
mixed_score / uncertain_sentence_ratio | Fraction of sentences with 0.45 ≤ P(AI) ≤ 0.55 |
source_attribution | Vendor hints when passage is AI (≥50 words) |
sentence[].* | sentence, prediction, ai_probability (and optional extras when the model returns them) |
radar_axes[] | AI scan & deep scan: five objects { key, label, value } with integer value 0–100 for polar charts; see axis table |
Privacy: scans processed in memory; benchmarks only under evaluation_results/.
From passage P(AI) vs server threshold ± mixed_offset (default 0.5 ± 0.05):
is_mixed: true on AI scan)On AI scan responses, confidence and ai_score are the same passage-level P(AI) (kept under both names only for backwards compatibility).
Derived from distribution_confidence (0.6 × passage certainty + 0.4 × sentence AI rate):
| Band | AI passage | Human passage |
|---|---|---|
| ≥ 0.75 | AI | Human |
| 0.55 – 0.75 | AIWrittenHumanModified | HumanWrittenAIModified |
| 0.45 – 0.55 | Uncertain | |
| < 0.45 | HumanWrittenAIModified | AIWrittenHumanModified |
When passage verdict is AI and text has ≥ 50 words: fused lmscan stylometrics + optional HF ChatGPT-detector.
UI should prefer source_display (single, dual, or uncertain) over raw candidates.
prediction — AI or Human (argmax).ai_probability — P(AI) for that window; 0.45–0.55 counts toward uncertain_sentence_ratio.radar_axes[])Five deterministic 0–100 integers for polar / spider charts—not extra classifier endpoints.
Derived from submitted text, passage-scale ai_score (P(AI)) and
the headline verdict probability produced during inference on the wire (aligned with Dashboard confidenceValue rounding).
key | Label | Interpretation |
|---|---|---|
ai_signal | AI Signal | Passage AI tendency: ai_score × 100, capped 0–100. |
uniformity | Uniformity | Similarity of sentence lengths (inverse of coefficient of variation). Higher = more regular lengths; lower = more human-like variation. |
confidence | Confidence | Verdict-facing facet from headline probability, scaled to one decimal percent then 0–100 (matches product radar). |
perplexity | Perplexity (proxy) | Lexical heuristic (CTTR-style diversity plus average English token length), not token-level LM perplexity. |
burstiness | Burstiness | Spacing variance of repeated words (gap variance vs average gap). Higher suggests more “bursty” natural rhythm vs flat repetition. |
For UI parity, render radar_axes alongside headline verdict, deep labels (when present), and sentence tallies so charts stay in sync with copy and meters.
/docs · Schema /api/v1/openapi.json