Orivfy Text Detection API

Frontend integration guide: stable legacy routes, additive document scan and citation verification, REST payloads, WebSockets, and secret_key authentication. Relative API prefix: /api/v1.

Old vs new · read this first

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.

Before this update (already integrated)

Existing capabilityExisting endpointDecision
Passage AI detectionPOST /api/v1/scan/ai-scanKEEP AS-IS No request or response change.
Sentence/deep AI detectionPOST /api/v1/scan/deep-scanKEEP AS-IS No request or response change.
Document text extractionPOST /api/v1/scan/extract-document-textKEEP AS-IS Still used when the frontend only needs extracted text.
Document metadata forensicsPOST /api/v1/scan/document-forensicsKEEP AS-IS This remains a separate metadata/editing analysis.
Streaming text explanationWS /api/v1/explanation/ws/explain-textKEEP 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 in this update (frontend opt-in)

New capabilityNew endpointFrontend change
Document scan — improved authenticity, provenance, OCR, forensic and text-model evidencePOST /api/v1/scan/document-scan
GET /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 batchPOST /api/v1/scan/document-scan/batchAdditive multi-file endpoint. It returns one normal job per file, polled through the existing job endpoint.
Document authenticity old product namePOST /api/v1/scan/document-authenticityCompatibility alias only. New frontend code should display “Document scan” and call the preferred endpoint.
Citation verification from textPOST /api/v1/citation/verify
GET /api/v1/citation/jobs/{job_id}
Add as an independent asynchronous feature.
Citation verification from filePOST /api/v1/citation/verify-documentOptional PDF/DOCX/TXT/Markdown upload entry point; poll the same citation job endpoint.

What “no downtime” means here

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.

Authentication

Every REST request must include secret_key matching the server SECRET_KEY in .env. Invalid or missing key: 403.

WebSockets: include secret_key in the first JSON frame.

Endpoints overview

MethodPathPurpose
GET/This integration page (HTML)
GET/api/v1/discovery.jsonDiscovery JSON (machine-readable index)
POST/api/v1/scan/ai-scanPassage-level Human / AI / Uncertain — scores, radar_axes[], optional source_attribution
POST/api/v1/scan/deep-scanSentence-level windows — dual verdicts, radar_axes[] (passage-aligned), scores, labels
POST/api/v1/scan/extract-document-textExtract plain text from PDF / DOCX / txt
POST/api/v1/scan/document-forensicsDocument metadata forensics (PDF / DOCX)
POST/api/v1/scan/document-scanQueue one improved document scan (PDF, DOCX, or document image)
POST/api/v1/scan/document-scan/batchQueue 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/verifyQueue legal/academic citation verification from text
POST/api/v1/citation/verify-documentQueue citation verification from PDF / DOCX / text document
GET/api/v1/citation/jobs/{job_id}Poll citation verification result
WS/api/v1/explanation/ws/explain-textStreaming forensic text explanation
GET/docsSwagger UI
GET/api/v1/openapi.jsonOpenAPI 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.

AI scan (passage)

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

POST/api/v1/scan/ai-scan

Request JSON

{
  "secret_key": "<server SECRET_KEY from .env>",
  "text": "Your passage …",
  "config": null
}
  • secret_key — Required; must match server SECRET_KEY.
  • text — Required UTF-8 string.
  • config — Optional ParameterConfig (threshold, batch_size, context_sentences, …).

Response JSON · AIScanResponse (illustrative)

{
  "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
  }
}
  • mixed_score — Always 0 on AI-only passage scan.
  • radar_axes — Always five facets (key, label, integer value); see Radar axes.

Deep scan (sentences)

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

POST/api/v1/scan/deep-scan

Request JSON

{
  "secret_key": "<server SECRET_KEY from .env>",
  "text": "Your document …",
  "config": null
}

Response JSON · DeepScanResponse (illustrative)

{
  "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 — Same rule as AI scan: Human · AI · Uncertain.
  • distribution_verdict — Sentence-mix label (see semantics).
  • deep_labels[passage_verdict, distribution_verdict].
  • ai_score / human_score — Passage-level P(AI) / P(Human); no duplicate “overall_*” aliases.
  • distribution_confidence — Verdict-facing blend (passage + sentence AI rate).
  • radar_axes — Five chart facets aligned with passage model + text heuristics; see Radar axes.

Documents

POST/api/v1/scan/extract-document-text

Multipart: secret_key (form) + file — PDF, DOCX, txt, md. Returns text, word_count, filename.

POST/api/v1/scan/document-forensics

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": "…"
}
POST/api/v1/scan/document-scan

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.

202 job envelope

{
  "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
}

Poll with X-API-Key

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.

POST/api/v1/scan/document-scan/batch

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.

Citation verification

POST/api/v1/citation/verify

Request JSON

{
  "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.

POST/api/v1/citation/verify-document

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.

No-downtime frontend migration

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.

Compatibility matrix

IntegrationStatusFrontend action
POST /api/v1/scan/ai-scanUNCHANGEDKeep the existing request and response handling.
POST /api/v1/scan/deep-scanUNCHANGEDKeep the existing request and response handling.
POST /api/v1/scan/extract-document-textUNCHANGEDKeep as the text-extraction utility.
POST /api/v1/scan/document-forensicsUNCHANGEDKeep as metadata/editing forensics; it has not been replaced.
WS /api/v1/explanation/ws/explain-textUNCHANGEDKeep the current explanation WebSocket integration.
POST /api/v1/scan/document-scanNEWAdd the asynchronous document-scan flow described below.
POST /api/v1/scan/document-scan/batchNEWUse only for multi-file uploads; single-file clients remain unchanged.
POST /api/v1/scan/document-authenticityCOMPATIBILITY ALIASDo not use for new screens; retained while old naming is retired.
POST /api/v1/citation/verifyNEWAdd independently from document scan.

Shared asynchronous job lifecycle

  1. Create: POST the text or file. Accept HTTP 202 and save job_id.
  2. Poll: GET the module’s job URL every 1–2 seconds using X-API-Key.
  3. Continue: while status is queued or processing, display stage_message.
  4. Complete: when status is done, read the module payload from result.
  5. Fail safely: when status is error, stop polling and display error with a retry action.
  6. Timeout: stop client polling after the UI’s chosen limit; preserve job_id so the user can retry status later.

Copy-ready frontend helper (TypeScript)

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));
  }
}

Document scan frontend flow

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;

Document scan batch flow

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:

OutcomeSuggested UI labelImportant behavior
verifiedVerified proofOnly display as verified when the response has trusted cryptographic/issuer proof.
likely_manipulatedLikely manipulatedShow evidence and require manual review.
likely_ai_generatedLikely AI-generatedThis concerns content origin; it is not by itself proof of issuer fraud.
suspicious_automated_generationSuspicious automated generationShow supporting metadata and require review.
no_decisive_manipulation_evidenceNo decisive manipulation evidenceNever shorten this to “Verified” or “Genuine.”
manual_reviewManual review requiredShow decision_basis.reason and evidence[].

Citation verification frontend flow

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;

Recommended release sequence

  1. Backend first: deploy and smoke-test discovery/OpenAPI plus the new routes. Leave all existing frontend calls untouched.
  2. Frontend code, feature off: ship job types, polling helper, result components, and retry/error states behind flags.
  3. Internal rollout: enable Document scan for staff/test users and compare displayed evidence with raw API responses.
  4. Public document rollout: enable gradually; keep the old Document authenticity alias available during cached-client rollout.
  5. Citation rollout: enable separately because it uses external providers and may have different latency/error behavior.
  6. Rename UI: after validation, change visible copy from “Document authenticity” to “Document scan.” Do not redirect or remove stable legacy scan routes.

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.

WebSocket · explanations

Open WebSocket → send one JSON text frame. Use wss: when API is HTTPS.

WS/api/v1/explanation/ws/explain-text

First message JSON (derive from 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
    }
  ]
}
  • For explanations, reuse passage P(AI) as confidence (same number as scan ai_score); include distribution_prediction when using deep-scan mode.
  • Server excerpts high-P(AI) snippets for Gemini; keep text empty or short.
  • You may copy additional fields returned by POST …/deep-scan (same shape as REST), including radar_axes — the WS handler ignores facets it does not need.

Frames from server

  • start — beginning
  • chunkcontent, partial_text
  • completefinal_text
  • errormessage

Field semantics

FieldMeaning
prediction / passage_verdictHuman · AI · Uncertain (passage-level)
distribution_verdictSentence-mix label (see table below)
deep_labels[passage_verdict, distribution_verdict]
is_aiTrue when passage is AI or Uncertain (AI scan only)
distribution_confidenceBlended 0–1 score for distribution verdict (deep scan)
confidence (AI scan)Passage P(AI); same numeric value as ai_score
ai_scorePassage P(AI) (recommended for new clients)
mixed_ratioFraction of sentences classified AI
mixed_score / uncertain_sentence_ratioFraction of sentences with 0.45 ≤ P(AI) ≤ 0.55
source_attributionVendor 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/.

Labels & confidence

Passage verdict (AI scan & deep scan)

From passage P(AI) vs server threshold ± mixed_offset (default 0.5 ± 0.05):

On AI scan responses, confidence and ai_score are the same passage-level P(AI) (kept under both names only for backwards compatibility).

Distribution verdict (deep scan only)

Derived from distribution_confidence (0.6 × passage certainty + 0.4 × sentence AI rate):

BandAI passageHuman passage
≥ 0.75AIHuman
0.55 – 0.75AIWrittenHumanModifiedHumanWrittenAIModified
0.45 – 0.55Uncertain
< 0.45HumanWrittenAIModifiedAIWrittenHumanModified

Source attribution

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.

Sentence rows

Radar axes (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).

keyLabelInterpretation
ai_signalAI SignalPassage AI tendency: ai_score × 100, capped 0–100.
uniformityUniformitySimilarity of sentence lengths (inverse of coefficient of variation). Higher = more regular lengths; lower = more human-like variation.
confidenceConfidenceVerdict-facing facet from headline probability, scaled to one decimal percent then 0–100 (matches product radar).
perplexityPerplexity (proxy)Lexical heuristic (CTTR-style diversity plus average English token length), not token-level LM perplexity.
burstinessBurstinessSpacing 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.

Swagger · OpenAPI

/docs · Schema /api/v1/openapi.json