Skip to content

Fileverity API docs

Document checks in an intake workflow

Add document checks to an intake flow: upload, receive the webhook, route by risk level, queue human review, store the report ID and verify it later.

View as MarkdownUpdated 2026-11-04

This recipe puts document checks in front of a process that receives documents from outside: loan applications, insurance claims, vendor onboarding or expense reports. Each document is checked once, routed by its risk level, and its report ID is kept as the audit record. The API returns a level and a recommended action. Your policy and your reviewers make the decision.

It uses the model fvdoc-3 with the calls from Files, Analyses and Webhooks. With a test key the upload and the check return an instant sample, but no webhook is sent and there is no sample PDF. Use Send test on a webhook endpoint in the Console to try your receiver.

The flow

  1. Upload the file with POST /files and start the check with POST /analyses. Put your case ID in metadata and send an Idempotency-Key.
  2. Answer your user at once. Documents can be queued, so do not hold their request open; the webhook tells you when the check is done.
  3. Receive analysis.completed or analysis.failed, verify the signature and queue a job.
  4. In the job, fetch the analysis, route it by its summary, and save the result and the PDF report with the case.
  5. Send cases that need a person to a review queue, with the PDF attached.
  6. Later, verify the report against the stored file.

1. Upload and start the check

Files up to 4 MB go up as multipart. For larger files, up to 50 MB, use a pre-signed upload as described in Files. Creating the analysis returns 202 with status: processing (a test key returns 200 with a finished sample), and your metadata comes back on the analysis and in every webhook for it.

FILE_ID=$(curl -s https://api.fileverity.com/v1/files \  -H "Authorization: Bearer $FILEVERITY_API_KEY" \  -H "Idempotency-Key: intake-A-10492-file" \  -F file=@application.pdf | jq -r .id) curl https://api.fileverity.com/v1/analyses \  -H "Authorization: Bearer $FILEVERITY_API_KEY" \  -H "Content-Type: application/json" \  -H "Idempotency-Key: intake-A-10492-check" \  -d "{\"model\": \"fvdoc-3\", \"file\": \"$FILE_ID\", \"metadata\": {\"case_id\": \"A-10492\"}}"
{  "id": "an_8Hk2pQ4vN9sL1xR7cW3mY6zB",  "object": "analysis",  "created": 1793700120,  "model": "fvdoc-3-2026-11-03",  "livemode": true,  "status": "processing",  "file": {    "name": "application.pdf",    "bytes": 18874368,    "format": "pdf"  },  "report_id": null,  "report_url": null,  "summary": null,  "result": null,  "error": null,  "progress": {    "stage": "queued",    "done": 0,    "total": 0,    "position": 1  },  "usage": {    "amount": 599,    "currency": "usd",    "funding": "balance"  },  "metadata": {    "case_id": "A-10492"  },  "completed_at": null,  "expires_at": null}

Save id with the case. Because the keys are derived from the case ID, running submit again after a timeout returns the first responses instead of uploading a second file or billing a second check. Keys are kept for 24 hours; see Idempotency.

2. Receive the result

Verify the signature with the verify function from Webhooks, saved as verify.js or verify.py. Answer 2xx quickly, and do the work in a job. enqueue stands for your own job queue. Store each job under the event ID, so a duplicate delivery does not create a second job.

import express from "express";import { verify } from "./verify.js"; const app = express(); app.post("/webhooks/fileverity", express.raw({ type: "application/json" }), async (req, res) => {  let event;  try {    event = verify(process.env.FILEVERITY_WEBHOOK_SECRET, req.headers, req.body.toString("utf8"));  } catch {    return res.sendStatus(400);  }  if (event.type === "analysis.completed" || event.type === "analysis.failed") {    await enqueue({ eventId: event.id, analysisId: event.data.id });  }  res.sendStatus(200);});

3. Route by risk level

summary is the stable, machine-readable verdict of a document check. level runs from L1 to L5 and action is the recommended next step. Route on both, and treat any value you do not recognize as a case for a person.

SummaryWhat it meansExample route
L1 or L2, acceptNo signs of tampering were found. L1 means the source was verified.Continue automatically. Store the report ID.
L1 or L2, noteNo signs of tampering, but AI-written text or an AI label is marked in the report.Continue, and flag the case if your policy limits AI-written content.
L3, reviewNeeds manual review.Standard review queue.
L4, reviewHigh risk.Priority review queue.
L5, rejectProblems are established in the report.Priority review queue. A person confirms before anything is declined.
status is failed or canceledThe check did not finish and you were not charged.Upload the stored original again, or send the case to a person.

The mapping is your policy. Move L3 into the priority queue, or let note go to review, by changing the function. The action set also contains resubmit, which the function treats as a request for another copy of the file. Every summary field is described in Analyses.

export function route(analysis) {  if (analysis.status !== "succeeded") return "manual";  const { action, level } = analysis.summary;  if (action === "accept") return "accept";  if (action === "note") return "accept_with_note";  if (action === "resubmit") return "request_new_copy";  if (action === "reject" || level === "L4" || level === "L5") return "priority_review";  return "review";}

4. Store the report ID and the PDF

The job fetches the analysis, routes it and saves what you will need later. It reuses call, route and the credentials from the earlier steps, and saveCase and saveReport stand for your own storage. Download the PDF in the same job: the analysis result is kept for 7 days, the page images and text excerpts of a document for 3 hours, and Fileverity keeps only the verification record afterwards, not the full report.

export async function handleResult(analysisId) {  const analysis = await call(`/analyses/${analysisId}`, { headers: auth });  const decision = route(analysis);  await saveCase(analysis.metadata.case_id, {    analysisId: analysis.id,    reportId: analysis.report_id,    reportUrl: analysis.report_url,    summary: analysis.summary,    model: analysis.model,    decision,  });  if (analysis.status === "succeeded") {    const pdf = await fetch(`${base}/analyses/${analysis.id}/report.pdf`, { headers: auth });    await saveReport(analysis.report_id, Buffer.from(await pdf.arrayBuffer()));  }  return decision;}
  • report_id (FV-XXXX-XXXX-XXXX) is the key to the audit record. Save it with the case, in your database and on any decision letter.
  • model is the dated snapshot that produced the result, so you can say later exactly which version checked the document.
  • Keep the original file in your own storage. Fileverity deletes an uploaded file when the analysis starts, and a failed check needs a new upload. See Data retention and security.

5. Run the human review queue

Create one task per routed case. Give the reviewer what they need to decide without calling the API again:

  • The case ID, level, action, reason and the finding IDs in findings, with the PDF report attached. The PDF lists each finding with its location and evidence.
  • The original file you stored.
  • report_url, the public verification page. It confirms the report record and the file fingerprint, but it is not a substitute for reading the report.

Record the reviewer, the decision and the time in your own system, next to report_id.

A clean check is not proof that a document is genuine, and a finding is not proof of fraud. Do not decline a case on the API result alone. For income, identity or ownership documents, confirm with the issuer through a contact you found yourself. The guide to document fraud detection covers what to check by document type.

6. Verify the report later

Months later, an auditor or a customer can check the record without a Fileverity account. Open the stored report_url. It shows the report type, the recorded finding and next step, the SHA-256 fingerprint of the file that was examined and the RFC 3161 timestamp. Drop your stored original on the page and your browser computes its SHA-256 and compares it with the record; nothing is uploaded. Any re-saved, converted or edited copy gives a different fingerprint.

You can run the same comparison in your own system:

sha256sum application.pdf

Verification does not analyze the file again, and it does not certify that a document is authentic. It records which file was examined, what the report said and when. The full report exists only in the PDF you stored. The fields of the report are described in the report object.

Before you go live

  • Build against a test key. It returns an instant sample, level L2 with action accept, without running the model. Webhooks and the PDF download need a live key, which you can try on your free start. Pass the other summaries from Analyses through route to test each branch.
  • Handle 503 service_unavailable: wait the retry-after seconds and retry with the same Idempotency-Key. See When the service is busy.
  • Set a monthly spend limit on the key, and add a balance.low webhook, so intake does not stop with insufficient_balance. See Pricing and billing.
  • Call the API from your server and keep keys in a secrets manager, never in browser code.
  • Quote the x-request-id header when you contact support.