Skip to navigation

Verify Overview

Beta

Verify is in beta. The endpoints return 404 NOT_FOUND unless Verify is enabled for your organization. Contact support@extend.ai to request access. Verify runs are not metered during the beta, so usage is null on every run.

Verify checks one document for evidence of fraud: whole-document fabrication, localized edits, and suspicious file internals. You submit a PDF, JPEG, PNG, or WebP and get back a document-level riskLevel (LOW, MEDIUM, or HIGH), a plain-language summary, and the detected signals, each with typed, locatable evidence that a reviewer can check against the file. Use it to screen bank statements, pay stubs, invoices, identity credentials, and any other document a person could alter before uploading.

Verify analyzes the exact bytes you submit. It combines deterministic checks of file metadata and PDF internals (signatures, revision history, hidden layers, fonts) with detection models for image manipulation and AI generation, then an assessment model grades each detection in the context of the whole document. It does not perform identity verification, compare documents to each other, or claim that a document is genuine: a LOW risk level means the checks that ran found nothing, not that the document is authentic.

Quick start

We’ll verify a bank statement that has been tampered with. It is the sample statement from the Parsing quick start, digitally signed with a “no changes allowed” certification signature, then edited after signing to delete one row (a $15.00 incoming wire fee) from the Fees and Other Withdrawals table on page 6, leaving the printed section total unchanged. For this quick start we’ve uploaded the file here.

Grab a key from the Developers page and store it as the EXTEND_API_KEY environment variable. If you’re using an SDK, see the installation instructions.

export EXTEND_API_KEY="your_api_key_here"

The /verify endpoint takes a file and an optional config. With no config, it uses the default verify_agentic engine, which parses the document and runs every supported check.

from extend_ai import Extend
client = Extend()
run = client.verify(
file={
"url": "https://extend-public-files.s3.us-east-2.amazonaws.com/bank_statement_tampered.pdf",
}
)
print(run.output.risk_level, run.output.summary)

Want to verify your own document? Upload it first, then pass the returned file id instead of a url. Verify accepts PDF, JPEG, PNG, and WebP files.

with open("bank_statement.pdf", "rb") as f:
uploaded = client.files.upload(file=f)
run = client.verify(file={"id": uploaded.id})

Example response

This is the response the run above returns, with repetitive evidence locations trimmed. Verify found the edit three different ways: the file’s revision history shows page 6 replaced after the signature, the signature check shows that the certification forbade any change, and the content check shows the fee rows no longer add up to the printed total. A fourth signal, about font subsets and clipped overflow text, was detected but graded LOW because the assessment recognised it as ordinary template behaviour. The sample statement is also missing page 2 of 10, which the content check reports as a page-number gap.

{
"object": "verify_run",
"id": "vr_AZMlpuOhBFhHc2NC6g4qd",
"file": {
"object": "file",
"id": "file_JspRZAP5B7G3AQAfGJlpW",
"name": "bank_statement_tampered.pdf",
"type": "PDF",
"parentFileId": null,
"metadata": { "pageCount": 7 },
"dataRetention": { "mode": "workspace_default", "status": "available" },
"createdAt": "2026-10-05T13:34:20.773Z",
"updatedAt": "2026-10-05T13:34:30.523Z"
},
"status": "PROCESSED",
"output": {
"riskLevel": "HIGH",
"summary": "This statement shows strong signs of material alteration: page 6 was replaced after a no-changes-allowed digital signature, and that same page contains a $171.00 mismatch in the Fees and Other Withdrawals section with an apparent missing row. The submission also omits page 2 of the 10-page statement, so the missing pages should be obtained before relying on it.",
"signals": [
{
"id": "vs_oqPSnnYrbSsI0ozvyohto",
"type": "REVISION_AND_PRODUCTION_HISTORY",
"riskAssessment": { "type": "LEVEL", "level": "HIGH" },
"summary": "The PDF was revised after signing, and the later revision replaced the content of page 6 rather than making only routine metadata updates.",
"evidence": [
{
"evidenceId": "meta.revision_history:001",
"type": "history",
"data": {
"events": [
{ "code": "PDF_REVISION_CREATED", "locationIds": ["rev_0"] },
{ "code": "PDF_REVISION_APPENDED", "locationIds": ["rev_1"] },
{ "code": "METADATA_CHANGED", "locationIds": ["rev_1", "obj_292_r1"] },
{
"code": "SIGNATURE_APPLIED",
"locationIds": ["sig_296"],
"timestamp": "D:20261005092239-04'00'",
"actor": "Extend Verify Test Signer",
"values": [
{ "name": "permitted_changes", "raw": "NONE" },
{ "name": "field_name", "raw": "Signature1" }
]
},
{ "code": "PDF_REVISION_APPENDED", "locationIds": ["rev_2"] },
{ "code": "METADATA_CHANGED", "locationIds": ["rev_2", "obj_292_r2"] },
{ "code": "PAGE_CONTENT_REPLACED", "locationIds": ["rev_2", "obj_49_r2", "obj_49_r1"] }
]
},
"locations": [
{ "locationId": "rev_0", "byteRange": { "offset": 0, "length": 482736 } },
{ "locationId": "rev_1", "byteRange": { "offset": 482736, "length": 6701 } },
{ "locationId": "sig_296", "objectRefs": ["296 0 R"], "byteRange": { "offset": 483489, "length": 5609 } },
{ "locationId": "rev_2", "byteRange": { "offset": 489437, "length": 3917 } },
{ "locationId": "obj_49_r2", "page": { "number": 6 }, "objectRefs": ["49 0 R"], "byteRange": { "offset": 489437, "length": 3353 } },
{ "locationId": "obj_49_r1", "page": { "number": 6 }, "objectRefs": ["49 0 R"], "byteRange": { "offset": 26639, "length": 3981 } }
/* 2 more locations: the metadata object in each revision */
]
}
]
},
{
"id": "vs_JZ6QC3TYP3SZEdQjhpScj",
"type": "DIGITAL_SIGNATURE_INTEGRITY",
"riskAssessment": { "type": "LEVEL", "level": "HIGH" },
"summary": "A no-changes-allowed signature was followed by later edits that replaced a statement page. The signature certificate is self-signed, which is not suspicious on its own but offers limited identity assurance.",
"evidence": [
{
"evidenceId": "meta.modified_after_signing:001",
"type": "history",
"data": {
"events": [
{
"code": "SIGNATURE_APPLIED",
"locationIds": ["sig_296"],
"timestamp": "D:20261005092239-04'00'",
"actor": "Extend Verify Test Signer",
"values": [
{ "name": "permitted_changes", "raw": "NONE" },
{ "name": "field_name", "raw": "Signature1" }
]
},
{ "code": "METADATA_CHANGED", "locationIds": ["rev_2", "obj_292_r2"] },
{ "code": "PAGE_CONTENT_REPLACED", "locationIds": ["rev_2", "obj_49_r2", "obj_49_r1"] }
]
},
"locations": [
{ "locationId": "sig_296", "objectRefs": ["296 0 R"], "byteRange": { "offset": 483489, "length": 5609 } },
{ "locationId": "rev_2", "byteRange": { "offset": 489437, "length": 3917 } },
{ "locationId": "obj_49_r2", "page": { "number": 6 }, "objectRefs": ["49 0 R"], "byteRange": { "offset": 489437, "length": 3353 } },
{ "locationId": "obj_49_r1", "page": { "number": 6 }, "objectRefs": ["49 0 R"], "byteRange": { "offset": 26639, "length": 3981 } }
/* 1 more location: the metadata object in the final revision */
]
},
{
"evidenceId": "meta.certificate_or_timestamp_invalid:001",
"type": "validation",
"data": {
"findings": [
{
"code": "SELF_SIGNED_SIGNER",
"values": [
{ "name": "certificate", "raw": "signer, Extend Verify Test Signer", "locationIds": ["sig_296_contents"] }
]
}
]
},
"locations": [
{ "locationId": "sig_296", "objectRefs": ["296 0 R"], "byteRange": { "offset": 483489, "length": 5609 } },
{ "locationId": "sig_296_contents", "byteRange": { "offset": 483502, "length": 5200 } }
]
}
]
},
{
"id": "vs_wm9njEdG4mEFFk8n1k5wR",
"type": "PDF_LAYERS_TEXT_AND_FORMS",
"riskAssessment": { "type": "LEVEL", "level": "LOW" },
"summary": "The font-subset differences and clipped text match common PDF template and layout behavior, including repeated overflow text at the page edge, and do not by themselves indicate fraud.",
"evidence": [
/* 6 structural_pattern items: two SECOND_SUBSET_OF_SAME_FACE font findings and four TEXT_CLIPPED_AWAY findings for a reference number that overflows the page edge, each with boundingBox locations */
]
},
{
"id": "vs_TbroCORqGt8mudBAfSJQS",
"type": "FINANCIAL_RECONCILIATION_AND_SEQUENCE_CONTINUITY",
"riskAssessment": { "type": "LEVEL", "level": "HIGH" },
"summary": "The Fees and Other Withdrawals detail does not add up to its printed total, with a $171.00 gap and a blank row where an entry appears missing. The submitted statement also skips page 2 of 10, so the missing pages should be obtained and reviewed.",
"evidence": [
{
"evidenceId": "content.arithmetic_or_ledger_mismatch:001",
"type": "validation",
"data": {
"findings": [
{
"code": "STATED_TOTAL_DOES_NOT_MATCH_COMPONENTS",
"values": [
{
"name": "finding",
"raw": "The Fees and Other Withdrawals rows add up to $7,049.00, but the section total says $7,220.00, a difference of $171.00. A blank row sits where an entry should be.",
"locationIds": ["loc_1", "loc_2"]
}
]
}
]
},
"locations": [
{ "locationId": "loc_1", "page": { "number": 6, "width": 612, "height": 792 }, "boundingBox": { "left": 496.476, "top": 283.356, "right": 532.714, "bottom": 504.007 } },
{ "locationId": "loc_2", "page": { "number": 6, "width": 612, "height": 792 }, "boundingBox": { "left": 490.903, "top": 536.062, "right": 531.914, "bottom": 546.257 } }
]
},
{
"evidenceId": "content.sequence_or_continuity_gap:001",
"type": "validation",
"data": {
"findings": [
{
"code": "PAGE_NUMBER_GAP",
"values": [
{
"name": "finding",
"raw": "The page footers go from \"Page 1 of 10\" straight to \"Page 3 of 10\", so page 2 of the 10-page statement is missing.",
"locationIds": ["loc_1", "loc_2"]
}
]
}
]
},
"locations": [
{ "locationId": "loc_1", "page": { "number": 1, "width": 612, "height": 792 }, "boundingBox": { "left": 464.782, "top": 762.372, "right": 502.827, "bottom": 770.019 } },
{ "locationId": "loc_2", "page": { "number": 2, "width": 612, "height": 792 }, "boundingBox": { "left": 464.724, "top": 762.358, "right": 502.43, "bottom": 770.011 } }
]
}
]
}
]
},
"failureReason": null,
"failureMessage": null,
"metadata": null,
"metrics": { "pageCount": 7, "processingTimeMs": 85681 },
"config": {
"engine": "verify_agentic",
"engineVersion": "0.1.0",
"advancedOptions": { "requireParsing": true }
},
"usage": null,
"source": "API",
"sourceId": "apireq_Ui6c6cyBVw7nWKqUCJKGt",
"createdAt": "2026-10-05T13:34:21.108Z",
"updatedAt": "2026-10-05T13:35:46.991Z"
}

A document with nothing detected returns "riskLevel": "LOW", "summary": "No fraud signals were detected.", and an empty signals array.

Key fields

FieldWhat it contains
output.riskLevelDocument-level risk: LOW, MEDIUM, or HIGH. The highest signal-level assessment, or LOW when nothing was detected.
output.summaryA plain-language explanation of the findings, written for a reviewer.
output.signalsDetected Public Signals only. Nothing is returned for checks that found nothing, could not run, or do not apply.
signals[].typeWhich signal category was detected, such as DIGITAL_SIGNATURE_INTEGRITY.
signals[].riskAssessmentThe risk attributed to that signal: { "type": "LEVEL", "level": "HIGH" }.
signals[].evidenceTyped, immutable observations behind the signal, each with locations in the submitted file (page number, bounding box, PDF object references, or byte range).
configThe configuration that ran, with engineVersion resolved to a concrete release and advancedOptions.requireParsing saying whether the document was parsed.
statusPROCESSING, PROCESSED, FAILED, or CANCELLED.

For every field, see Response Format. For full request/response details, see the Verify File API reference.

Use the output

Route on riskLevel, then surface the signals and their summaries to whoever reviews the document.

if run.output.risk_level == "HIGH":
send_to_fraud_review(run.id)
for signal in run.output.signals:
print(f"{signal.type} ({signal.risk_assessment.level}): {signal.summary}")
for item in signal.evidence:
print(" evidence:", item.evidence_id, item.type)

Sync vs async

The example above calls the synchronous /verify endpoint, which times out after 5 minutes on both engines. A timed-out request usually fails with 504 Gateway Timeout and no runId, while the run keeps processing in the background. verify_agentic runs often take longer than that. For production workloads, use the asynchronous /verify_runs endpoint: it returns a run in the PROCESSING state immediately, and you poll GET /verify_runs/{id} until status is PROCESSED, FAILED, or CANCELLED. GET /verify_runs lists past runs with their risk levels, and DELETE /verify_runs/{id} removes a run and the data derived from its document.

Webhook events for verify runs are not available yet, and the SDKs do not yet include a create_and_poll / createAndPoll helper for verify runs. Poll the run as shown below. See Async Processing for the general pattern.

import time
run = client.verify_runs.create(
file={"url": "https://extend-public-files.s3.us-east-2.amazonaws.com/bank_statement_tampered.pdf"}
)
while run.status == "PROCESSING":
time.sleep(2)
run = client.verify_runs.retrieve(run.id)
if run.status == "PROCESSED":
print(run.output.risk_level, run.output.summary)
else:
print(run.status, run.failure_reason, run.failure_message)

What Verify checks

Findings are reported as Public Signal categories, a stable enum. The current engine release runs checks in these categories:

SignalDetects
FILE_FORMAT_AND_PDF_STRUCTUREFiles disguised as another type, broken or rebuilt PDF structure, content after the end of the file, and files that read differently to different PDF software.
DIGITAL_SIGNATURE_INTEGRITYContent changed after signing, signatures that do not match the file they are attached to, and signer certificates or timestamps that did not hold up when the signature was made.
PROVENANCE_AND_ISSUER_MARKERSContent credentials that fail to verify, and provenance watermarks whose manifest has been stripped.
REVISION_AND_PRODUCTION_HISTORYFiles re-saved after issuance, backdated timestamps, interactive image editors in the production chain, and scanned pages encoded differently from the rest.
CAPTURE_CONVERSION_AND_LAUNDERINGAnnotation layers that hide or change what a reader sees.
PDF_LAYERS_TEXT_AND_FORMSValues masked and retyped, image patches pasted over a scan, hidden text, form values that differ from what the form displays, values set in a font the rest of the line does not use, and an OCR text layer that disagrees with the page.
IMAGE_COMPOSITING_AND_RECONSTRUCTIONRegions spliced, cloned, erased, or painted over, detected by an image manipulation model.
AI_GENERATED_CONTENTGenerator watermarks, content credentials that assert generation, and pages or regions a detection model judges to be synthetic.
CROSS_FIELD_AND_SEMANTIC_CONSISTENCYDates that contradict each other, and names, addresses, identifiers, or issuers stated differently in different places. Found by a content agent reading the parsed document; verify_agentic only.
FINANCIAL_RECONCILIATION_AND_SEQUENCE_CONTINUITYTotals that do not add up, running balances that do not reconcile, and gaps in page numbers, statement periods, or transaction order. verify_agentic only.

Configuration

The quick start uses default settings. To control a run, pass a config object alongside file. For the full reference, see Configuration.

Engine

{ "config": { "engine": "verify_light" } }
EngineWhen to use
verify_agenticParses the file and runs every check, including the checks that read the parse, and leverages an agent harness optimized for tamper detection. Slower and more expensive, but the most performant option for detecting difficult cases of document fraud.
verify_lightFaster checks that do not require parsing, but will use parsed results if the file has already been parsed in Extend. Covers all checks except the agentic checks around content and OCR. Add "advancedOptions": { "requireParsing": true } to include the parse-backed checks.

Parsing on the light engine

verify_light skips the parse for speed. Set advancedOptions.requireParsing to add the checks that read the parse, such as the comparison of a scan’s hidden text layer against the page. verify_agentic always parses and rejects false.

{ "config": { "engine": "verify_light", "advancedOptions": { "requireParsing": true } } }

Supported inputs

  • File types: PDF, JPEG, PNG, and WebP. Other types fail with FILE_TYPE_NOT_SUPPORTED. A file that claims to be a PDF but is not still runs, because the mismatch is itself a signal.
  • Input methods: a public url or an Extend file id.
  • Data retention: a verify run inherits the retention mode of its file. For zero-retention files the result is delivered once, in the sync response or the first GET, and purged afterwards, so store what you need from that response. DELETE /verify_runs/{id} removes any run’s derived data on demand.

Via Extend Studio

Studio includes a Verify playground where you can run a file, browse past runs, and inspect each signal’s evidence highlighted on the document. Open Verify in Studio (it is visible only to organizations enrolled in the beta). Studio runs use the same engine, configuration options, and result shape as the API.

What Verify is not

  • Not identity verification. Verify says nothing about whether the person submitting a document is who they claim to be, and it never links a document to a person.
  • Not cross-document analysis. Each run looks at one file. It does not compare submissions to each other or to previously seen documents.
  • Not a certificate of authenticity. Signals are returned only when detected. An empty signals array means the checks that ran found nothing; it does not mean every check applied, or that the document is genuine.

Next steps