Skip to main content
Every key delivered in a file.enrichment.success payload, with the type of its value and a short example. For delivery and signature verification, see Webhooks.

How to read this page

Nested keys are written as paths. file_info.mimetype means a mimetype key inside the file_info object. A [] means the key holds an array, so modules[].module_id is the module_id of each entry in modules. Types are described plainly: string, number, boolean, object, array, and null. Two qualifiers matter:
Where a key is marked “may be absent or null”, do not distinguish the two. Both forms occur, and checking for only one will break on the other.
Keys are added over time. Ignore any key you do not recognise rather than rejecting the payload.

Top level

event_id is the same on every redelivery of an event, which makes it the right value for recognising a duplicate. Treat it as an opaque string.
version is "2", a payload schema version rather than a semantic version. Compare it exactly rather than parsing it.
case_label and file_label are human-readable labels for the case and file. file_label falls back to the name parsed from the S3 upload key when no file label is available; case_label has no such fallback. Each field is an empty string (never null) if nothing could be resolved. They were added to notifications going forward; payloads sent before this addition do not include them.
Timestamps carry a UTC offset and microsecond precision, as in 2026-08-25T17:21:40.912430+00:00, rather than a Z suffix.

file_info

mimetype is detected from the contents of the file rather than taken from the upload, so it reflects what the file actually is even when it was sent with a misleading name or content type. Three families of file are analysed:
Word documents are analysed in the .docx format only, which is why a single value covers them. The legacy .doc format is not supported, and neither are spreadsheets, presentations, or any other format outside the three families above. An unsupported file produces no success payload, so no webhook is delivered for it. See Webhooks for how to detect a file that did not complete.
raw_metadata holds whatever metadata the file itself carried, so its keys depend on the file type. The three shapes below share no keys, so check mimetype before reading it.

Images

metadata.tags is free-form: its keys are whatever the file carried, so read the ones you care about by name and expect the rest to vary between files. The c2pa group appears only when the file carries a content credentials manifest.

PDFs

Word documents

analysis_metadata

Raw output from each step of the analysis. Every key below is always present, and is null when that step does not apply to the file, so an image has no pdf_structure and a PDF has no ela.

What decides whether a section is filled in

Two things, in order. First the file type, which determines whether a step can run at all: ela is calculated for JPEG images only, because it measures compression artefacts that exist only in a lossy format, and pdf_structure and annotations need a PDF. Then the content of the file, which determines whether a step that ran produced anything: barcode_results is null when no barcode could be read, and a scanned or photographed PDF yields no structural analysis because it carries no text to measure.
A PDF with no usable text reports that in one of two ways. pdf_structure is null when the analysis was not attempted, and an object whose document_summary holds only an error when it was attempted and found nothing. Both mean the same thing to you, so treat a section as usable only when it is non-null and document_summary.error is absent. Checking for null alone will let the second form through, and the font keys you then read will be missing.
The same two steps decide which entries appear in modules. A module that depends on one of these sections runs only when that section was produced, so a PNG has no ela and therefore no ela_fraud_detection module, and a file with no readable barcode has no barcode_analysis module. Read results.modules_run for what actually ran rather than assuming a fixed set.
A null section is not a failed one. It means that part of the analysis did not apply to this file, which is the normal case for most files. Where a step was expected and did not complete, a caveats entry says so.

ela

Error Level Analysis, which looks for uneven compression across an image.

thumbnail

barcode_results

For barcode types that encode personal data, value is redacted and the readable content appears in fields instead.

ocr

Content read from the file, and the entities recognised in it. classifications[].custom_guess is filled in when category is "Other". The available categories depend on the document classes configured for your organisation. Entities, in key_persons, key_organizations, key_locations and key_reference_numbers: raw_ocr_text is filled in when a reading correction was applied, and holds the original text. legible is false when the value could not be read confidently from the image. Dates, in key_dates:
An unread date component is not evidence. When precision is "month" or "year", the finer parts of date_value are placeholders. When legible is false or year_inferred is true, the named component was not read from the document. A check that depends on such a component is unverifiable rather than passing: an illegible expiry year never means “not expired”.

pdf_structure

dominant_font and dominant_size each pair a value with its occurrence count. font_usage is keyed by font name and size_usage by point size written as a string. document_summary.error appears instead of every other key in document_summary, when the analysis ran but found no text to measure. When it is present, potential_anomalies is an empty array and none of the font or size keys exist, so read it before any of them. A PDF with no usable text can also arrive as a null pdf_structure instead; see What decides whether a section is filled in.

annotations

Comments, stamps and other annotation objects carried by a PDF, and whether any of them were added after the document itself was created. highlighted_pdf is the path of a copy of the PDF with each reported annotation boxed, and is null when there are no findings. It holds the same value as artifacts.annotated_pdf. Unlike the other timestamps in the payload, analysis_timestamp carries no UTC offset. Each entry in annotations.analysis.findings:
risk_level is either "High" or "Unknown", which is a different scale from the "Informational" to "High" severity used elsewhere in the payload. Do not compare the two directly.
risk_level is "High" when the annotation’s own timestamp is later than the document’s creation date, meaning it was added after the document was made. It is "Unknown" in every other case, including when either date is missing, so treat "Unknown" as undetermined rather than as safe. The three technical_details strings are never null. When a value is absent from the annotation a placeholder appears instead, so annotation_text reads "No Content", annotation_author reads "Unknown Author" and annotation_date reads "Unknown". A document that genuinely contains the text “Unknown Author” is therefore indistinguishable from one where the author was not recorded. annotation_date is written as YYYY-MM-DD HH:MM:SS with no timezone, rather than in the ISO 8601 form used elsewhere in the payload.
coordinates is [x0, y0, x1, y1] measured from the bottom-left of the page, following the PDF convention. The bbox in pdf_structure.potential_anomalies[].location is measured from the top-left instead. The two cannot be drawn onto a page render with the same code.
Routine annotation objects are left out, including form fields, popup containers and ordinary hyperlinks. A link is reported when it has no destination or carries its own appearance, because that is the shape of a box drawn over existing content. Findings whose boxes overlap are combined into one enclosing box, so a single entry can cover several stacked annotations. A PDF carrying an annotation added after it was created:
A PDF with no reportable annotations, which is the common case:

results

The report written for a reviewer.
results.overall_risk_score and every severity are title-case, as in "High" and "Informational". Compare them exactly.
results.findings holds only the findings that fired, summarised for a reviewer: source_analysis names a modules[].module_id, and custom_alert_type is normally set when alert_type is "OTHER". Either can be null — see Tracing a finding back to its evidence.

modules

One entry per part of the analysis that ran. Which modules appear depends on the file type, its content, and the modules enabled for your organisation, so both the length and the order of this array vary between analyses.
Look modules up by module_id, and findings by alert_type. Do not read either array by position.
When modules[].error is set, that module did not complete. Its findings are absent rather than clean, and a caveats entry will normally say so. modules[].findings holds one entry for every check the module ran, whether or not it fired:
Filter on found being true to get the alerts that actually triggered. The entries where found is false are the record of what was checked and came back clean, which is useful evidence in itself.
Values of module_id you may see include content_verification, manipulation_detection, document_consistency, software_anomalies, reverse_search_findings, pdf_structure_anomaly, barcode_analysis, reference_validation and ela_fraud_detection. Read results.modules_run rather than assuming a fixed set.

Evidence

Findings are backed by two kinds of evidence, which carry different weight. search_evidence is web research, so it corroborates. verification_evidence is a direct check against an authoritative registry or API, so it confirms or contradicts. Keep them distinct when you display them.

search_evidence

verification_evidence

Carries checks only when your organisation’s verification setting was on for the analysis. For an analysis run with it off, a module’s verification_evidence is null or has an empty checks list, results.verification_evidence_summary is null, and no finding carries a v reference.
is_valid has three states. true means confirmed and false means contradicted, but null means could not be verified, which is not the same as invalid. It usually means the authoritative source was unreachable or does not cover the subject, and a caveats entry will normally say so. Presenting null as a negative result will mislead your reviewers.
outputs holds fields specific to whichever source answered, so its keys vary with provider. attests states in one line what a verdict from that source actually proves. extracted is the value as read from the document, before it was normalised into subject.

Tracing a finding back to its evidence

Evidence references live on modules[].findings only. The findings in results summarise them for a reviewer and carry none. From a module finding, its evidence_refs point into its own module:
  • q1.1 matches a results[].result_id in that module’s search_evidence. The number after the dot starts at 1, and matches the [1] markers in that query’s answer.
  • v1 matches a checks[].check_id in that module’s verification_evidence.
Ids restart in each module, so resolve a reference against its own module rather than a combined index. An empty evidence_refs on a finding that fired means no external source is linked, usually because the finding rests on the file’s own metadata. From a summary finding, source_analysis gives you the module and alert_type the finding inside it, with custom_alert_type narrowing an "OTHER". That finding’s evidence_refs then resolve as above.
That match is not guaranteed. A module can report more than one finding for the same alert_type, and source_analysis can be null or name a module that is not in modules[]. Where it is ambiguous or does not resolve, use the finding’s supporting_evidence rather than guessing at a match.
The two evidence fields on a finding do different jobs: evidence[] is written to be read, evidence_refs[] is the link. Reference ids never appear in evidence[].detail.

Cross-module summaries

The same evidence rolled up across modules, so you can show an overview without walking every module. resolved counts checks that got an authoritative answer. failed counts those that could not be verified, which is the is_valid of null case above. Each entry under per_module holds queries, results and cited_result_ids, or checks, in the same shapes as above. Three things to expect when you compare a summary against the modules it came from:
  • The entries are copies, not digests. per_module["x"] holds the same queries and results as the modules[] entry for x.
  • cited_result_ids covers search only. It is the union of that module’s q* references. v* references are left out, and verification has no equivalent field.
  • A module can be missing from a summary. Search lists a module that searched and found nothing; verification lists one only if it ran at least one check. Neither lists a module whose evidence gathering timed out.
Where no module used a lane at all, that summary is null rather than an object with zero counts.

artifacts

Files produced by the analysis, given as storage paths rather than URLs. Exchange the file_id for a short-lived signed URL with Get Asset URL, passing the matching asset_type.

integrity_signals

A summary of the forensic verdict, so you can route a file without walking every module. Each boolean has three states:
Do not treat null as false. “We did not determine this” and “we determined this is not the case” carry very different weight in a fraud decision.

caveats

Limits on how far an analysis should be trusted, such as an authoritative source being unreachable or text being hard to read. Always present; an empty array means no caveats apply. Caveats sit alongside the findings rather than being findings themselves.
A critical caveat on a low-risk analysis is the case worth surfacing: it means the clean verdict itself is not dependable.
type is an open list, so handle values beyond the examples above gracefully. scope_ref names whatever the caveat attaches to, such as a check_id.

Reserved fields

Some keys arrive in the payload but are not part of the published contract. They exist for Veridox diagnostics, may change without notice, and should be ignored:
This list is not exhaustive, which is why it is worth ignoring keys you do not recognise rather than rejecting the payload.