Not in Good Order (NIGO) Attachment Validation checks the documents your signers upload against the document type you asked for. When a signer uploads an attachment, OneSpan Sign classifies the file, optionally extracts a defined set of fields from it and runs content checks over them, and — where enabled for your account — inspects PDFs for signs of alteration.
This topic is intended for senders, operations and compliance users. It describes OneSpan Sign 26.R5. For where validation results appear in the API, how to retrieve them, and how to handle each error and "no results" state, see NIGO Attachment Validation (API and SDK).
Advisory only, and non-blocking
No check in this feature prevents a signer from doing anything. A wrong document type, a failed check, an unreadable image, a tamper signal, or an outage of the validation service all leave the signer's experience unchanged: the upload succeeds, the attachment requirement is satisfied, and the signer can complete the transaction.
Validation produces signals for human review. Acting on them — asking for a replacement document, rejecting a transaction, escalating to compliance — is a decision you make outside the feature.
Extraction is opt-in, and off by default
Two controls on each attachment requirement decide how much validation it receives:
Choosing a Document type turns on classification for that requirement.
Additionally ticking Verify document turns on extraction and the content checks that run over the extracted fields.
Extraction is off unless you tick it, on each requirement individually. There is no account-wide setting that turns it on for every attachment.
Enabling the feature for an account
NIGO Attachment Validation is off by default for every account. It is enabled in Backoffice, under the account's Features page. Three settings control it, and all three default to off:
Feature setting | What it enables |
|---|---|
Attachment Classification | Detecting what an uploaded document actually is, and comparing that to the expected type. Shows the Document type control and the validation results in the Sender UI |
Attachment Data Extraction | Pulling defined fields out of the document and running the content verification checks. Shows the Verify document control |
Document Authenticity | Inspecting PDFs for signs of alteration. In 26.R5 the findings are available through the API only — see Document tamper detection |
The three settings are layered, and the dependency is enforced when you save: Document Authenticity requires Attachment Data Extraction, which requires Attachment Classification. Attempting to break the chain returns "Document Authenticity requires Attachment Data Extraction to be enabled first." or "Attachment Data Extraction cannot be disabled while Document Authenticity is enabled."
The feature as a whole also sits behind a release flag that OneSpan enables for your account.
Requesting an attachment with an expected document type
In the recipient's Attachments dialog, each attachment row has a Document type dropdown, a Verify document checkbox and a Required checkbox. The first two only appear when the corresponding account features are enabled. Required is unrelated to validation — it controls whether the signer must upload something at all.
Choose the Document type you expect the signer to upload. That expected type is what every later result is measured against — without it, nothing is classified and no results are produced.
Seventeen document types are supported in 26.R5:
Document type | Label |
|---|---|
PASSPORT | Passport |
DRIVERS_LICENSE | Driver's License |
BANK_STATEMENT | Bank Statement |
UTILITY_BILL | Utility Bill |
TAX_RETURN | Tax Return |
PAY_STUB | Pay Stub |
INVOICE | Invoice |
CONTRACT | Contract |
T4_SLIP_CA | T4 Slip (CA) |
STATE_ID_CARD_US | State ID Card (US) |
VOID_CHEQUE_CA | Void Cheque (CA) |
VOID_CHEQUE_US | Void Cheque (US) |
AUTO_INSURANCE_CERTIFICATE | Auto Insurance Certificate |
PROPERTY_INSURANCE | Property Insurance |
LIFE_INSURANCE_POLICY | Life Insurance Policy |
VEHICLE_REGISTRATION | Vehicle Registration |
EMPLOYMENT_LETTER | Employment Letter |
Setting an expected type does not change what the signer is asked to do, and does not restrict what they are able to upload. It only establishes what the uploaded file will be compared against.
Turning on extraction
Tick Verify document on the requirement. The checkbox is disabled until a Document type has been chosen, because extraction is defined per document type. When it is on, OneSpan Sign extracts the fields defined for that type and runs the verification checks over the extracted values.
Extraction is skipped, and no fields or check results are produced, whenever the requirement does not have Verify document ticked, classification could not identify the document, the detected type does not match the expected type, classification reported a quality problem with the file, or classification was not confident enough — extraction is only performed on a high-confidence classification.
Reading the results
Results are visible only to the sender and others with edit access to the transaction. Signers never see extracted data, check results, confidence, or tamper findings.
On the transaction page, each file a signer has uploaded shows a status badge under its name, and a Verification result button that opens the results for that file:
Badge | Meaning |
|---|---|
Detected type named, with a check mark | The document was recognised as the type you asked for at high confidence, and no check has reported a warning or failure |
Detected type named, with a warning, flagged for review | A type was detected (or shows as unknown), but either it was not reported as a match — see the mismatch callout below — or at least one check reported a warning or failure |
Type not identified, with a warning, flagged for review | Classification produced no type at all: the file had a quality problem, or the file was never validated (see below) |
Reviewed | You have marked the file as reviewed. This replaces the other badges |
The Verification result dialog shows, for the file:
A summary of the checks. Any file problem is listed first as a failure. Then a document type line, reported as a match or a mismatch alongside the type that was detected. Then one line per content check, marked as passed, warning or failed, in plain language — for example, the expiry date that was found and has passed. Passing lines are collapsed by default.
The extracted data, as a list of field names and values — dates in your locale's format and country codes as country names — or a message that no extracted data is available.
A preview of the file, and the Mark as reviewed button (see Mark as Reviewed).
Extraction runs after the upload and takes a little longer than classification. Until it completes, the dialog reports no extracted data.
Not shown in the Sender UI in 26.R5, though available through the API: the classification confidence, the reason extraction did not run, document tamper findings, and who reviewed a file and when.
When the detected type doesn't match what you asked for
Nothing is blocked. The upload stands and the attachment requirement remains satisfied.
On the transaction page the file is flagged for review. In the results dialog the document type line reads as a mismatch, alongside the type that was actually detected, so you can see what the signer sent instead. Extraction is skipped for that file.
The signer sees a single message on their upload indicating the document does not appear to be what was requested. They are not prevented from continuing, are not asked to re-upload, and receive no email or notification.
Two different things read as "mismatch". A genuinely wrong document — the detected type is not the one you requested — and the right document detected at medium or low confidence, where the detected type is the one you requested but a match is only reported at high confidence. Neither case is extracted or checked: extraction is only performed on a high-confidence classification, so in both cases the dialog shows no extracted fields. The badge and the document-type line name the detected type but never show the confidence, so the badge alone does not tell you which case you are in. Compare the named type with what you requested: the same type reported as a mismatch means low confidence — the signer very likely sent the right document, and a clearer copy may let it be recognised; a different type means the wrong document.
When a document can't be classified
Three distinct situations, all non-blocking:
The document type could not be determined. The file was readable but did not match any supported type. The file is flagged for review with its type shown as unknown, and extraction is skipped.
The file had a quality problem. The file was rejected before classification could be attempted — for example it was too blurry, too dark or too low-resolution, had glare, was corrupted, was too large or too small, or was an unsupported format. Validation accepts PDF, Word, JPEG, PNG, TIFF, BMP and GIF files between 1 KB and 10 MB, and images of at least 200 × 200 px. The specific problem appears as the first line of the results summary, and extraction is skipped. This is the one situation where the signer also sees a message describing the actual problem with their file, which they may act on by uploading a better copy.
The validation service was unavailable. No result is recorded for the file at all. On the transaction page it is flagged as type not identified; its results dialog reports that there is no result, and does not offer Mark as reviewed. The upload itself is unaffected, but validation will not run for the file later; if you need it validated, ask the signer to replace it.
Verification checks
Verification checks run over extracted field values, so they require Verify document to be on and extraction to have succeeded. Each check reports Pass, Warning or Fail, and names the fields it looked at.
Check | What it does | Outcomes |
|---|---|---|
Data completeness | Confirms the fields that matter for this document type were found and are not blank | Pass — all present. Fail — one or more missing, and the missing field names are listed |
Expiry | Confirms a document that carries an expiry date has not passed it | Pass — not expired. Fail — expired. Warning — the date was absent or could not be read |
Recency | Confirms a dated document is recent enough to be useful | Pass — within the window. Fail — older than the window. Warning — the date was absent or could not be read |
Coverage period | For insurance documents, confirms today falls inside the policy's coverage window | Pass — in force. Fail — coverage has ended. Warning — coverage has not started yet, or a date was absent or unreadable |
Tax year recency | For T4 slips, confirms the tax year is recent enough | Pass — within range. Fail — too many years behind. Warning — the year was absent, non-numeric, or implausible |
A missing or unreadable date produces a Warning, never a Fail. Only a date that was successfully read and found to be out of range produces a Fail.
Which checks run, and against which fields, depends entirely on the document type:
Document type | Data completeness requires | Date check |
|---|---|---|
Passport | fullName, dateOfBirth, expiryDate | Expiry on expiryDate |
Driver's License | fullName, dateOfBirth, expiryDate, address | Expiry on expiryDate |
Bank Statement | accountHolderName, financialInstitutionName, statementDate | Recency on statementDate, 6 months |
Utility Bill | fullName, address, providerName, billDate | Recency on billDate, 6 months |
Tax Return | fullName, taxYear, totalIncome | — |
Pay Stub | fullName, employerName, payDate, netPay | Recency on payDate, 6 months |
Invoice | invoiceDate, vendorName, totalAmount | Recency on invoiceDate, 12 months |
Contract | contractTitle, partyName1, effectiveDate | — |
T4 Slip (CA) | employeeName, employerName, taxYear, employmentIncome | Tax year recency on taxYear, 2 years |
State ID Card (US) | fullName, dateOfBirth, expiryDate, idNumber | Expiry on expiryDate |
Void Cheque (CA) | accountHolderName, bankName, accountNumber, transitNumber, institutionNumber | — |
Void Cheque (US) | accountHolderName, bankName, accountNumber, routingNumber | — |
Auto Insurance Certificate | policyHolder, insurer, policyNumber, coverageStart, coverageEnd | Coverage period |
Property Insurance | policyHolder, propertyAddress, insurer, policyNumber, coverageStart, coverageEnd | Coverage period |
Life Insurance Policy | policyOwnerName, insuredName, insurer, policyNumber, issueDate | Recency on issueDate, 10 years |
Vehicle Registration | ownerName, vin, licensePlate, registrationExpiry | Expiry on registrationExpiry |
Employment Letter | employeeName, employerName, jobTitle, letterDate | Recency on letterDate, 3 months |
Document tamper detection
When Document Authenticity is enabled for the account, OneSpan Sign additionally inspects PDF attachments for signs that the file was altered after it was issued: whether the tool that produced the PDF is consistent with a genuine issued document, whether content was appended after the file was first written, whether the file's own timestamps are consistent, and whether the file was modified long after the date printed on it. It runs for eleven of the seventeen document types — the statements, bills, returns, invoices, contracts and insurance and registration documents — and not for photo identity documents, void cheques or employment letters.
In 26.R5 the findings are returned through the API only. The results dialog does not display them. Integrators can read them from the verifications endpoint; see Document integrity in the API topic for the checks, their outcomes and the overall rating.
Limitations — what it does not detect
This is a set of heuristics over a PDF's structure and metadata. It is not fraud detection, and it cannot establish that a document is genuine.
It does not read the content of the document to judge whether the content is true. It cannot tell you that an income figure was altered, that a name is not the signer's, or that the document was issued to someone else.
It is largely blind on scanned and photographed documents. A PDF that is entirely a scanned image supports only the check on the producing tool; every other check reports as not assessed. A photo or screenshot uploaded directly as an image supports none of the checks. Because a great many legitimate attachments are scans or phone photos, not assessed is a common and expected outcome — and it is not a pass.
Detection of the producing tool reads only the PDF's own metadata. A file whose producer and creator fields have been stripped, or set to something unrecognised, reports as not assessed rather than a failure. Anyone able to edit a PDF is also able to edit those fields.
A finding is not proof, and the absence of findings is not proof of authenticity. Re-saving a bank statement through a browser's print-to-PDF, or downloading a statement months after its stated period, are ordinary behaviours that can produce a warning.
Mark as Reviewed
Mark as Reviewed records your formal decision that you have reviewed an uploaded file and accept it.
One action, four names. The button in the 26.R5 results dialog reads Mark as reviewed, a reviewed file shows a Reviewed badge, the API field is reviewed, and the audit trail records the action as Mark as Verified and its undo as Undo Verification. These are all the same action. This documentation calls it Mark as Reviewed.
It is available for any file that has a validation result, whatever that result says — extraction off, a mismatch, a failed check, a quality problem. In the Sender UI the button is in the results dialog; a file that was never validated has no result, and so no button. Through the API the action is always available, including for a file with no result — see Mark as Reviewed through the API.
Marking a file as reviewed records who marked it and when. It does not override, clear, re-run or change any classification, extracted data, verification check or tamper finding — those results remain exactly as they were, alongside the record of your review.
Undo review
Undoing a review is informational only. It records who undid it and when, and preserves the original review record so the full history remains visible. It does not request a new document from the signer, does not notify the signer, does not re-run classification, extraction or any check, and does not change any stored result.
If a file is replaced
If a signer replaces an uploaded file, the review state does not carry over. The new file starts unreviewed and must be reviewed on its own.
Audit trail
The following attachment actions are recorded in the transaction's audit trail:
Upload File Attachment
Download File Attachment
Mark as Verified — the audit-trail name for Mark as Reviewed
Undo Verification — the audit-trail name for Undo review
Validation results are not recorded in the audit trail in 26.R5. Classification outcomes, extracted field values, verification check results and tamper findings do not produce audit entries. The audit trail records the human decision — that a sender marked a file as reviewed — not the machine findings that informed it.
Not in Good Order (NIGO) Attachment Validation classifies the documents your signers upload, optionally extracts a defined set of fields from them and runs content checks over those fields, and — where enabled — inspects PDFs for signs of alteration. This topic covers how to request that validation, how to retrieve the results, and how to handle every outcome.
This topic is intended for SDK and API users building on attachment validation. It describes OneSpan Sign 26.R5. For information on requesting an expected document type and reviewing validation results from the Sender UI, see NIGO Attachment Validation (Sender UI).
Advisory only, and non-blocking
No validation outcome affects the transaction. A wrong document type, a failing check, an unreadable file, a tamper signal, or an outage of the validation service will never cause an upload to be rejected, an attachment requirement to remain unsatisfied, or a signer to be prevented from completing. Treat every field described here as advisory, and never gate your own workflow on the validation service being reachable.
There is no classification or extraction endpoint
OneSpan Sign does not expose /classify or /extract, or any other endpoint that runs validation on demand. Classification and extraction are internal, and are triggered as a side effect of a signer uploading or replacing an attachment. You cannot submit a document for validation yourself, re-run validation on an existing document, or validate a document that is not an attachment on a transaction.
Your integration has exactly two touch points: you configure what should be validated when you build the transaction, and you read the results afterwards.
Requesting validation
Both settings live on an attachment requirement, which is nested inside a role. Set them when you create or update the role, or when you create the package.
Field | Type | Default | Effect |
|---|---|---|---|
attachmentType | string enum | null | The expected document type. Setting it is what enables classification for this requirement. |
extractionEnabled | boolean | null (off) | Enables field extraction and the content verification checks for this requirement. Has no effect unless attachmentType is also set. |
{
"id": "role1",
"type": "SIGNER",
"signers": [
{ "email": "jane@example.com", "firstName": "Jane", "lastName": "Doe" }
],
"attachmentRequirements": [
{
"id": "attachment-1",
"name": "Proof of address",
"description": "Upload a recent bank statement",
"required": true,
"attachmentType": "BANK_STATEMENT",
"extractionEnabled": true
}
]
}POST or PUT this to /aws/rest/services/packages/{packageId}/roles[/{roleId}], or include it in the package creation payload.
extractionEnabled is a nullable boolean: null and false are equivalent, and both mean off. It is nullable so that an omitted field can never accidentally enable extraction — only an explicit true does.
Validation errors
The request is rejected if you ask for something the account is not entitled to:
Condition | Error |
|---|---|
attachmentType is set and the DocInsight release flag is off for the account | error.validation.role.releaseFlagDocInsightIsDisabled — "Release flag DocInsight is disabled." |
attachmentType is set and the account feature Attachment Classification is off | error.validation.role.attachmentClassificationIsDisabled — "Attachment classification feature is disabled." |
extractionEnabled is true and the account feature Attachment Data Extraction is off | error.validation.role.attachmentDataExtractionIsDisabled — "Attachment data extraction feature is disabled." |
Tamper detection has no per-requirement field. It is controlled solely by the account-level Document Authenticity setting, and applies to every eligible attachment on the account.
Retrieving results
GET /aws/rest/services/packages/{packageId}/attachment/verificationsNote that the path segment is attachment, singular.
Response | Meaning |
|---|---|
200 OK | A bare JSON array of validation results, one element per uploaded attachment file |
204 No Content | No results exist for this transaction |
The response is a bare array, not an object with a named field. There is no pagination.
This endpoint requires transaction edit privileges — sender, owner or delegate. Signers receive 403. Validation results are not exposed anywhere in the signer-facing API.
Classification is synchronous; extraction is asynchronous. Classification completes during the upload, so classificationResult is available as soon as the upload returns. Extraction runs afterwards, and its result appears at this endpoint once it completes — until then the file's element reports extractionStatus: "NOT_PERFORMED" with a null reasonCode and no extractionResult. Results are assembled on each request: the extraction result is fetched from the validation service and the content checks are evaluated at that moment, so read this endpoint when you need current results rather than caching it.
Detecting completion
Extraction is asynchronous, no event announces its completion in 26.R5 (see Callbacks and events), and PACKAGE_ATTACHMENT fires before it finishes — so a client that needs extraction results has to re-read this endpoint until each file reaches a final state.
This endpoint is not rate limited in 26.R5, so limit yourself. Every call fetches the current extraction result for every validated file on the transaction from the validation service and re-evaluates the content checks. Do not loop tightly. Recommended: start on the PACKAGE_ATTACHMENT callback (per upload) or on SIGNER_COMPLETE (once the signer is done), re-read no more than once every 10 seconds per transaction — one call covers all of the transaction's files — and stop after 5 minutes, treating any file still without a final state as unavailable. These are recommendations, not measured limits: there is no published bound on extraction time.
Decide per file, on each read:
Element state | Action |
|---|---|
The file is absent from the array | Stop. It was never validated and will not appear later — see "No results" states |
extractionStatus is COMPLETED | Done |
extractionStatus is FAILED | Stop. Terminal — see reasonCode |
extractionStatus is NOT_PERFORMED and reasonCode is not null | Stop. Terminal, and the reason code says why |
extractionStatus is NOT_PERFORMED and reasonCode is null | Apply the decision tree under reasonCode. Keep reading only if it lands on its last branch, until your time limit |
If you only need the classification, there is nothing to wait for.
Response shape
Each array element:
Field | Type | Notes |
|---|---|---|
attachmentUuid | string | The id of the attachment requirement the file was uploaded against |
fileName | string | Base name only — the extension is stripped. To reconstruct the original filename, join fileName and extension with a dot (no dot when extension is empty). To match an element to an uploaded file, prefer fileId |
fileId | string | The file's id, as a string. It equals files[].id on the attachment requirement (an integer there) — the reliable key for matching an element to an uploaded file, whose files[].name carries the original filename |
extension | string | The file extension, without a leading dot |
classificationResult | object or null | What the document was detected as. null if the stored result could not be read |
extractionResult | object or null | Extracted fields, checks and integrity findings. null whenever extraction did not produce a result |
extractionStatus | string enum | COMPLETED, NOT_PERFORMED or FAILED. Never null |
reasonCode | string enum or null | Why extraction did not complete. Can be null even when extractionStatus is NOT_PERFORMED |
typeMatch | boolean | Whether the detected type matched what you asked for at high confidence — which is also the condition for extraction to be performed. It does not tell you whether extraction has completed — see below |
classificationResult:
Field | Type | Notes |
|---|---|---|
documentUuid | string | The internal validation id for this file |
documentType | string or null | The detected type. null when classification failed |
confidenceScore | number | 0.0 when classification failed |
confidenceLevel | string or null | HIGH, MEDIUM or LOW. null when classification failed |
providerName | string or null | |
failed | boolean | Always present, always a boolean. Check this before reading any other field on this object |
errorCode | string or null | A translation key describing the problem. Populated only when failed is true |
failureMessage | string or null |
extractionResult:
Field | Type | Notes |
|---|---|---|
documentUuid | string | |
extractedFields | object | String-to-string map. Sparse — see below. Empty unless extraction completed |
providerName | string or null | |
verificationCheckResults | array | The content check results. Empty unless extraction completed |
extractionStatus | string enum | Same as the element-level value |
reasonCode | string enum or null | Same as the element-level value |
failureMessage | string or null | |
preflight | object or null | Document integrity findings. Integrity analysis runs at the start of extraction, before fields are extracted, so this is present on an extraction that was submitted and then failed as well as on a completed one. It is never produced when extraction was not submitted — in those cases extractionResult itself is null, and there are no findings to read |
failed | boolean | Derived: true whenever extractionStatus is not COMPLETED |
Which extractionStatus and reasonCode to read. Both appear twice — on the array element and inside extractionResult. Read the element-level ones: they are always present. When extractionResult is non-null, its extractionStatus and reasonCode are identical to the element's (the element copies them from it), and its failed is simply extractionStatus not being COMPLETED. When extractionResult is null, the element-level fields are the only ones, and say why there is no result. One edge case: if the validation service ever reports a status this release does not know, the nested value is null and the element reports NOT_PERFORMED.
Null fields are present in the JSON, not omitted. Do not treat the presence of a key as evidence that it has a value. Conversely, new fields may be added in later releases — ignore keys you do not recognise.
typeMatch says whether extraction was eligible, not whether it has completed. It is true only when classificationResult.documentType equals your requested attachmentType and confidenceLevel is HIGH — and extraction is only performed under exactly those conditions. So typeMatch: false means no extraction result will ever exist for this file, for one of two reasons that reasonCode tells apart: a different documentType was detected (the wrong document — extraction was never submitted, and reasonCode stays null), or the right documentType was detected at MEDIUM or LOW confidence (extraction was submitted and declined, and reasonCode is CLASSIFICATION_QUALITY_WARNING with classificationResult.failed: false). Conversely typeMatch: true only means extraction was submitted — read extractionStatus to know whether it has completed.
Example — a successful result
[
{
"attachmentUuid": "attachment-1",
"fileName": "statement-june",
"fileId": "8842",
"extension": "pdf",
"classificationResult": {
"documentUuid": "9f2c1d84-3b7e-4a15-9c2d-6e8f1a4b7c30",
"documentType": "BANK_STATEMENT",
"confidenceScore": 0.95,
"confidenceLevel": "HIGH",
"providerName": "bedrock",
"failed": false,
"errorCode": null,
"failureMessage": null
},
"extractionResult": {
"documentUuid": "9f2c1d84-3b7e-4a15-9c2d-6e8f1a4b7c30",
"extractedFields": {
"accountHolderName": "JANE DOE",
"financialInstitutionName": "Bank of Example",
"statementDate": "2026-06-30",
"statementPeriodStart": "2026-06-01",
"statementPeriodEnd": "2026-06-30"
},
"providerName": "bedrock",
"verificationCheckResults": [
{
"ruleName": "data_completeness",
"fields": [],
"status": "PASS",
"message": "esl.error.attachment_verification_check.data_completeness_pass"
},
{
"ruleName": "date_validation",
"fields": ["statementDate"],
"status": "PASS",
"message": "esl.error.attachment_verification_check.date_recent"
}
],
"extractionStatus": "COMPLETED",
"reasonCode": null,
"failureMessage": null,
"preflight": {
"checks": [
{
"check": "SOURCE",
"status": "PASS",
"messageKey": "esl.error.preflight_check.source_verified",
"params": {}
},
{
"check": "EDITS_AFTER_CREATION",
"status": "PASS",
"messageKey": "esl.error.preflight_check.no_edits_after_creation",
"params": {}
},
{
"check": "FILE_DATES",
"status": "PASS",
"messageKey": "esl.error.preflight_check.file_dates_consistent",
"params": {}
},
{
"check": "PERIOD_DATE",
"status": "PASS",
"messageKey": "esl.error.preflight_check.period_date_within",
"params": {}
}
],
"verifiability": { "inputType": "VERIFIABLE_PDF" },
"tier": "CLEAR"
},
"failed": false
},
"extractionStatus": "COMPLETED",
"reasonCode": null,
"typeMatch": true
}
]Example — the file could not be classified
[
{
"attachmentUuid": "attachment-1",
"fileName": "photo",
"fileId": "8843",
"extension": "jpg",
"classificationResult": {
"documentUuid": "1a4b7c30-9f2c-4a15-8e6d-3b7e5c2d9f10",
"documentType": null,
"confidenceScore": 0.0,
"confidenceLevel": null,
"providerName": null,
"failed": true,
"errorCode": "esl.error.attachment_verification.image_too_blurry",
"failureMessage": "Document is difficult to read"
},
"extractionResult": null,
"extractionStatus": "NOT_PERFORMED",
"reasonCode": "CLASSIFICATION_QUALITY_WARNING",
"typeMatch": false
}
]Document types and extraction fields
Seventeen document types are supported. The Extraction fields column lists every key that can appear in extractedFields for that type. Keys marked (date) are returned in ISO YYYY-MM-DD form; every other value is returned as printed on the document.
attachmentType | Label | Extraction fields |
|---|---|---|
PASSPORT | Passport | fullName, dateOfBirth (date), expiryDate (date), issuingCountry, documentNumber |
DRIVERS_LICENSE | Driver's License | fullName, dateOfBirth (date), expiryDate (date), address, issuingCountry, licenseNumber, issuingState |
BANK_STATEMENT | Bank Statement | accountHolderName, financialInstitutionName, statementDate (date), address, statementPeriodStart (date), statementPeriodEnd (date) |
UTILITY_BILL | Utility Bill | fullName, address, providerName, billDate (date), utilityType |
TAX_RETURN | Tax Return | fullName, taxYear, netIncome, totalIncome, address, formType |
PAY_STUB | Pay Stub | fullName, employerName, payPeriodEndDate (date), payDate (date), netPay, grossPay, employeeAddress, payFrequency, ytdGrossPay, ytdNetPay |
INVOICE | Invoice | invoiceNumber, vendorName, invoiceDate (date), totalAmount |
CONTRACT | Contract | contractTitle, partyName1, partyName2, effectiveDate (date), endDate (date) |
T4_SLIP_CA | T4 Slip (CA) | employerName, employerAddress, employeeName, employeeAddress, taxYear, employmentIncome, incomeTaxDeducted, provinceOfEmployment |
STATE_ID_CARD_US | State ID Card (US) | fullName, dateOfBirth (date), expiryDate (date), address, idNumber, issuingState |
VOID_CHEQUE_CA | Void Cheque (CA) | accountHolderName, bankName, accountNumber, transitNumber, institutionNumber |
VOID_CHEQUE_US | Void Cheque (US) | accountHolderName, bankName, accountNumber, routingNumber |
AUTO_INSURANCE_CERTIFICATE | Auto Insurance Certificate | policyHolder, insurer, policyNumber, coverageStart (date), coverageEnd (date), vin, vehicleMake, vehicleModel, vehicleYear, licensePlate |
PROPERTY_INSURANCE | Property Insurance | policyHolder, propertyAddress, insurer, policyNumber, coverageStart (date), coverageEnd (date) |
LIFE_INSURANCE_POLICY | Life Insurance Policy | policyOwnerName, insuredName, insurer, policyNumber, issueDate (date) |
VEHICLE_REGISTRATION | Vehicle Registration | ownerName, ownerAddress, vehicleMake, vehicleModel, vehicleYear, vin, licensePlate, registrationExpiry (date), stateOrProvince |
EMPLOYMENT_LETTER | Employment Letter | employeeName, employerName, employerAddress, jobTitle, startDate (date), compensation, letterDate (date) |
Two fields are constrained to a fixed set of values:
utilityType— one ofelectric,gas,water,internet,mobile,otherpayFrequency— one ofweekly,bi-weekly,monthly,undefined
taxYear is a plain string, not a date. It is returned as printed on the document.
Note that classification can report a document as an unrecognised type, in which case documentType is "UNKNOWN". UNKNOWN is not a valid attachmentType and cannot be requested.
Handling extracted fields
extractedFields is sparse. It contains only the keys that were actually found and legible. A field that was absent from the document, unreadable, or that the extractor declined to guess at is omitted from the map entirely — it is not returned as null or as an empty string.
This applies to every key, including the ones the checks require. Your code must handle any key being missing, and must not assume that every key listed for a document type will be present in every response.
All values are strings.
Dates are normalized to ISO
YYYY-MM-DD. This normalization is performed by the extractor and constrained by its output schema, but it is not re-validated afterwards. Treat a well-formed ISO date as the expected case, and handle a value that does not parse rather than assuming it will.Monetary amounts are returned exactly as printed — for example
"65000.00","$75,000 per year", or"120,000 00". They are not normalized to numbers. Parse them yourself, defensively.Everything else is returned as printed on the document.
Extracted data is produced by the validation service and surfaced through this endpoint. It is not editable through the API, and there is no endpoint to correct or override an extracted value.
Verification check results
extractionResult.verificationCheckResults is an array of check results, present only when extraction completed. Each element:
Field | Type | Notes |
|---|---|---|
ruleName | string | Which check this is |
fields | array of strings | The extracted field names this check looked at. For a data_completeness failure this is the list of missing fields |
status | string enum | PASS, WARNING or FAIL |
message | string | A translation key describing the specific outcome |
There are four checks. Which of them run, and against which fields, is determined entirely by the document type.
ruleName | Purpose | Outcomes and message keys |
|---|---|---|
data_completeness | The fields that matter for this type were found and are non-blank | PASS → …_check.data_completeness_pass, with fields: []. FAIL → …_check.data_completeness_missing, with fields listing what is missing |
date_validation | A date is not expired (EXPIRY) or is recent enough (RECENCY) | PASS → …_check.date_valid or …_check.date_recent. FAIL → …_check.date_expired or …_check.date_too_old. WARNING → …_check.date_not_available (field absent or blank) or …_check.date_parse_error (value not a parseable ISO date) |
coverage_period | Today falls inside an insurance coverage window | PASS → …_check.date_valid. FAIL → …_check.date_expired (coverage ended). WARNING → …_check.coverage_not_started, …_check.date_not_available, or …_check.date_parse_error |
tax_year_recency | A tax year is recent enough | PASS → …_check.date_recent. FAIL → …_check.date_too_old. WARNING → …_check.date_not_available (absent, non-numeric, before 2000, or later than the current year) |
All message keys are prefixed esl.error.attachment_verification_check.
A missing or unparseable date always produces WARNING, never FAIL. Only a date that was read successfully and found to be out of range produces FAIL. Do not treat WARNING as a soft failure of the document — in most cases it means the check could not be performed.
Per-type configuration:
attachmentType | data_completeness requires | Date check |
|---|---|---|
PASSPORT | fullName, dateOfBirth, expiryDate | date_validation EXPIRY on expiryDate |
DRIVERS_LICENSE | fullName, dateOfBirth, expiryDate, address | date_validation EXPIRY on expiryDate |
BANK_STATEMENT | accountHolderName, financialInstitutionName, statementDate | date_validation RECENCY on statementDate, 6 months |
UTILITY_BILL | fullName, address, providerName, billDate | date_validation RECENCY on billDate, 6 months |
TAX_RETURN | fullName, taxYear, totalIncome | — |
PAY_STUB | fullName, employerName, payDate, netPay | date_validation RECENCY on payDate, 6 months |
INVOICE | invoiceDate, vendorName, totalAmount | date_validation RECENCY on invoiceDate, 12 months |
CONTRACT | contractTitle, partyName1, effectiveDate | — |
T4_SLIP_CA | employeeName, employerName, taxYear, employmentIncome | tax_year_recency on taxYear, 2 years |
STATE_ID_CARD_US | fullName, dateOfBirth, expiryDate, idNumber | date_validation EXPIRY on expiryDate |
VOID_CHEQUE_CA | accountHolderName, bankName, accountNumber, transitNumber, institutionNumber | — |
VOID_CHEQUE_US | accountHolderName, bankName, accountNumber, routingNumber | — |
AUTO_INSURANCE_CERTIFICATE | policyHolder, insurer, policyNumber, coverageStart, coverageEnd | coverage_period on coverageStart / coverageEnd |
PROPERTY_INSURANCE | policyHolder, propertyAddress, insurer, policyNumber, coverageStart, coverageEnd | coverage_period on coverageStart / coverageEnd |
LIFE_INSURANCE_POLICY | policyOwnerName, insuredName, insurer, policyNumber, issueDate | date_validation RECENCY on issueDate, 10 years |
VEHICLE_REGISTRATION | ownerName, vin, licensePlate, registrationExpiry | date_validation EXPIRY on registrationExpiry |
EMPLOYMENT_LETTER | employeeName, employerName, jobTitle, letterDate | date_validation RECENCY on letterDate, 3 months |
Note that data_completeness requires some fields the extractor treats as optional — payDate on a pay stub and totalIncome on a tax return are examples. A data_completeness failure naming those fields means they were not found on the document, not that something went wrong.
Document integrity (tamper detection)
extractionResult.preflight carries the document integrity findings. It is null unless all of the following hold: the account has Document Authenticity enabled, the document type is one of the eleven supported for integrity analysis, the file is a PDF or an image, and extraction was submitted. Because integrity analysis runs before field extraction, it is also present on a result whose extractionStatus is FAILED. It is absent, with extractionResult itself null, whenever extraction was not submitted — a type mismatch, an unrecognised type, a quality failure, or extraction not enabled — so no findings are lost in those cases; none were produced.
The eleven supported types are BANK_STATEMENT, UTILITY_BILL, TAX_RETURN, PAY_STUB, INVOICE, CONTRACT, T4_SLIP_CA, AUTO_INSURANCE_CERTIFICATE, PROPERTY_INSURANCE, LIFE_INSURANCE_POLICY and VEHICLE_REGISTRATION. Photo identity documents, void cheques and employment letters are excluded: the signals rely on the structure of an issued PDF and do not apply to a photograph of a card.
In 26.R5 this block is available through the API only. The Sender UI does not display integrity findings.
Four independent checks run over the file, and each reports one of four outcomes:
check | What it looks for |
|---|---|
SOURCE | Whether the tool recorded in the PDF's metadata as having produced or created it is consistent with a genuine issued document — as opposed to a consumer design tool, an online PDF editor, or an AI document generator |
EDITS_AFTER_CREATION | Structural evidence that content was appended or revised after the file was first written |
FILE_DATES | Whether the PDF's own creation and modification timestamps are internally consistent and not in the future |
PERIOD_DATE | Whether the PDF's last-modified timestamp is far later than the date printed on the document, and whether dates printed on the document contradict each other |
status | Meaning |
|---|---|
PASS | The check ran and found nothing wrong |
WARNING | The check ran and found something that casts doubt without proving alteration |
FAIL | The check ran and found a strong signal of alteration |
NOT_ASSESSED | The check could not run for this input, or ran and found no evidence to judge. Not a pass — render it distinctly |
{
"checks": [
{
"check": "SOURCE",
"status": "FAIL",
"messageKey": "esl.error.preflight_check.unexpected_source",
"params": { "tool": "Canva", "field": "CREATOR", "matchedValue": "WWW.CANVA.COM" }
}
],
"verifiability": { "inputType": "VERIFIABLE_PDF" },
"tier": "HIGH"
}Field | Notes |
|---|---|
checks | One entry per integrity check, always complete, ordered problems-first |
checks[].messageKey | A translation key for this specific outcome. Determined by which variant fired, not by the check — two failures of the same check can carry different keys, so resolve by messageKey, never derive a label from check alone |
checks[].params | Substitution values for a parameterized label, for example {"months": "7"}. Empty when the label takes none. Keys vary by outcome and may be added over time — read by key, never assume a fixed set |
verifiability.inputType | How much of the file could be inspected: VERIFIABLE_PDF (all four checks), IMAGE_ONLY_PDF (a scanned PDF — only SOURCE runs, the rest are NOT_ASSESSED), or NATIVE_IMAGE (a photo or screenshot — nothing runs) |
tier | The overall rating: CLEAR (every check ran and passed on a fully inspectable file), LOW, MEDIUM, or HIGH (enough alteration signals fired to warrant attention). A file that could not be fully inspected never rates CLEAR, however clean it looks |
Render tier as delivered. It is the single overall verdict, and it already accounts for how much of the document could be inspected as well as what the individual checks found. Do not re-derive it from the check statuses.
params.matchedValue on a SOURCE outcome is text taken from the uploaded document's own metadata. It is sanitized and truncated, but it originates from an untrusted file — HTML-escape it before rendering.
Limitations
Integrity analysis inspects a PDF's structure and metadata. It does not read the document's content to judge whether that content is truthful, and it cannot establish that a document is genuine. A finding is a prompt for human review; the absence of findings is not evidence of authenticity. Because most legitimate attachments are scans or phone photos, NOT_ASSESSED is a common and expected outcome. See Document tamper detection in the Sender UI topic for the full statement of what it does not detect.
Callbacks and events
Coming in 26.R6: ATTACHMENT_VALIDATION_RESULT
Preview. This describes a callback event planned for OneSpan Sign 26.R6. It is not available in 26.R5, and its details may change before release.
26.R6 adds a callback event, ATTACHMENT_VALIDATION_RESULT, subscribed to like any other event. It fires once per signer per transaction, once that signer's upload step is complete and every extraction for their files has reached an outcome. Its message is FINISHED when every file's extraction completed, or FAILED when at least one did not; sessionUser identifies the signer and packageId the transaction. The event announces that results are ready and carries no per-file detail — on receiving it, read the verifications endpoint. It removes the need to re-read the endpoint on a timer.
Error and status reference
extractionStatus
Value | Meaning |
|---|---|
COMPLETED | Extraction ran and produced results. extractedFields and verificationCheckResults are populated |
NOT_PERFORMED | Extraction has not run, or was never attempted. Check reasonCode for why |
FAILED | Extraction was attempted and errored |
reasonCode
Value | Cause | What to do |
|---|---|---|
null with extractionStatus: NOT_PERFORMED | Extraction was never requested for this file, was skipped because the detected type did not match, has not completed yet, or could not be submitted or read because the validation service was unreachable. | Work through the decision tree below. Only its last branch is worth waiting on |
EXTRACTION_NOT_ENABLED | The requirement asked for extraction, but the account feature Attachment Data Extraction was off when the file was uploaded. Normally prevented when the role is created; it can occur if the feature is turned off afterwards. A requirement that simply does not have extractionEnabled set produces a null reason code, not this one | Configuration, not a failure. Confirm the account feature, then have the signer replace the file |
CLASSIFICATION_UNKNOWN | The document could not be identified as any supported type | The signer likely uploaded the wrong thing, or an unsupported document. Human review |
CLASSIFICATION_UNSUPPORTED_TYPE | The document was identified, but that type does not support field extraction | Not expected in 26.R5 — every supported document type supports extraction. Reserved for future types |
CLASSIFICATION_QUALITY_WARNING | Two causes, told apart by classificationResult.failed. true: the file was rejected before classification — unreadable, wrong format, or out of size bounds — and classificationResult.errorCode names the problem. false: classification succeeded but with confidenceLevel MEDIUM or LOW, and the validation service declined to extract; documentType still shows what was detected | Ask the signer for a better copy. If failed is false and documentType is the type you requested, the signer very likely sent the right document; a clearer copy may let it be recognised |
EXTRACTION_ERROR | Extraction was attempted and errored. Always paired with extractionStatus: FAILED | Terminal for this file; re-reading will not change it. See What can and cannot be retried below |
Resolving a null reason code. Use the fields that are present to rule out the terminal causes before waiting on anything. classificationResult.failed never accompanies a null reason code — a failed classification always reports CLASSIFICATION_QUALITY_WARNING — so the tree starts from a successful classification:
classificationResultisnull— the stored classification could not be read, and extraction never ran. Terminal.The requirement did not have
extractionEnabled: true— extraction was never requested. Terminal, and expected.classificationResult.documentTypeis not theattachmentTypeyou requested — the detected type did not match, and extraction was never submitted. Terminal. (A correct type detected atMEDIUMorLOWconfidence does not reach this tree: it reportsCLASSIFICATION_QUALITY_WARNING.)Otherwise extraction was requested and no result is available yet: it is still running, it was refused at submission (for example the 1,024 px minimum for the six high-detail types), or the validation service could not be reached. In 26.R5 these are not distinguished. Re-read this endpoint at intervals with backoff, and after a bounded time of your choosing treat the extraction result as unavailable for this file.
What can and cannot be retried. OneSpan Sign runs classification and extraction once per file, at upload, and neither retries them nor exposes a way to re-trigger them. Re-reading this endpoint only picks up an extraction that is still in progress — branch 4 above. EXTRACTION_ERROR, and a classificationResult.errorCode of upstream_service_error, are stored outcomes for that file and will not change on re-read; the only recovery is for the signer to replace the file, which runs validation again from the start. If either recurs across many files, the validation service is degraded — contact OneSpan support.
Distinguish configuration from failure. EXTRACTION_NOT_ENABLED means the feature was not turned on — nothing went wrong. Every other reason code describes something that happened to the document.
Limits you can check before uploading
The limits below belong to the validation service, and are separate from OneSpan Sign's own attachment upload limits (about 16 MB, and the file types allowed on the account). Exceeding the upload limits rejects the upload. Exceeding the limits below does not — the upload succeeds and classification fails with the error code shown. Checking them on the client avoids those errors entirely.
Limit | Value | Error code if exceeded |
|---|---|---|
Accepted formats | PDF (application/pdf), Word (application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document), JPEG, PNG, TIFF, BMP, GIF. The type is determined from the file's content and name, not from a declared MIME type | unsupported_content_type |
Minimum file size | 1,024 bytes | file_too_small |
Maximum file size | 10,485,760 bytes (10 MiB) — lower than the 16 MB upload limit | file_too_large |
Minimum image dimensions | 200 × 200 px, for an image upload and for each of the first two pages of a PDF or Word file | image_resolution_too_low |
Maximum image dimensions | None. Images over 1,568 px on the longest side are downscaled | — |
Extraction applies a stricter minimum for six document types. T4_SLIP_CA, AUTO_INSURANCE_CERTIFICATE, PROPERTY_INSURANCE, LIFE_INSURANCE_POLICY, VEHICLE_REGISTRATION and EMPLOYMENT_LETTER require 1,024 × 1,024 px when extraction runs. A file between 200 and 1,024 px passes classification, then extraction is refused before it starts and nothing is recorded — the file reports extractionStatus: NOT_PERFORMED with reasonCode: null. If extraction is on for one of these types, enforce 1,024 px on the client.
The remaining image checks — image_too_blurry, image_too_dark, image_low_contrast, image_glare_detected — measure the pixels and cannot be pre-validated by size or type. Blur and darkness apply to every input; contrast and glare apply only to photo uploads (JPEG, PNG, TIFF, BMP, GIF), never to PDF or Word pages. A born-digital PDF containing no images skips them entirely. For photos, guide the signer to a flat, evenly lit, in-focus capture without flash reflection.
classificationResult.errorCode
Populated only when classificationResult.failed is true, and only with one of the values below. These are translation keys, all prefixed esl.error.attachment_verification.
Key suffix | Cause |
|---|---|
image_resolution_too_low | Below 200 × 200 px (1,024 px for the six high-detail types at extraction) |
image_too_blurry | The image is out of focus |
image_low_contrast | The image has insufficient contrast |
image_glare_detected | Glare obscures the document |
image_too_dark | The image is underexposed |
document_corrupted | The file could not be decoded as an image or document |
file_too_large | Over 10,485,760 bytes |
file_too_small | Under 1,024 bytes |
unsupported_content_type | Not one of the accepted formats listed above |
upstream_service_error | The validation service reported a problem with no mapping here — an empty file, a missing filename or content type, or an image that could not be processed. Stored for the file and unchanged on re-read — see What can and cannot be retried above |
On the upload response. The signer's client receives the same keys immediately, as classificationErrorCode on each file in the response to the attachment upload or replace call, so that it can display an appropriate message. That response can additionally carry one key that never appears on classificationResult.errorCode: type_mismatch — the detected type does not match the requested type. A file carries at most one key there; a quality problem takes precedence over a mismatch. On the verifications endpoint a mismatch is expressed by typeMatch and documentType instead. The upload response also carries verificationResultUuid, the same value as classificationResult.documentUuid.
"No results" states
Situation | What you see |
|---|---|
No attachment requirement on the transaction has an attachmentType | 204 No Content |
The DocInsight release flag is off for the account | 204 No Content |
The account feature Attachment Classification is off | 204 No Content |
The validation service was unreachable when the file was uploaded | The file is absent from the array, permanently. Classification runs once, at upload; if it could not run, no result is ever recorded for that file and it will not appear later. Other files may still appear |
The stored classification could not be read | The element is present with classificationResult: null |
An empty result is never an error. 204 means nothing has been validated on this transaction, which is the normal state for a transaction that does not use the feature.
Reconcile this array against the files you know were uploaded. Every uploaded file is listed in attachmentRequirements[].files[] on the role. A file in that list but absent from this array was never validated — the service was unreachable when it was uploaded, or validation was not configured for its requirement at the time. Treat that as an explicit not validated state and surface it as such: re-reading this endpoint will not change it, and the only way to validate the file is for the signer to replace it. Do not confuse it with a file that is present with extractionStatus: NOT_PERFORMED and a null reasonCode — resolve that one with the decision tree under reasonCode.
Mark as Reviewed through the API
PUT /aws/rest/services/packages/{packageId}/attachment/{attachmentId}/file/{fileId}/reviewed
DELETE /aws/rest/services/packages/{packageId}/attachment/{attachmentId}/file/{fileId}/reviewedPUT marks the file as reviewed; DELETE undoes it. Neither takes a request body. Both return 200 with the updated AttachmentFile, and both require transaction edit privileges. A fileId that does not belong to the given attachmentId returns 404 error.validation.AttachmentFileNotFound.
The API field name is reviewed, and the feature is Mark as Reviewed; the audit trail records it as Mark as Verified. For what the action means and does not do — always available, never re-runs or clears results, never notifies the signer — see Mark as Reviewed in the Sender UI topic.
Five fields on AttachmentFile carry the state. They are also returned on AttachmentRequirement.files[] wherever a role or package is read, so you can see review state without calling the verifications endpoint:
Field | Type | Notes |
|---|---|---|
reviewed | boolean | |
reviewedBy | string | The id of the user who marked it reviewed |
reviewedAt | timestamp | |
unreviewedBy | string | The id of the user who undid it |
unreviewedAt | timestamp |
Marking sets
reviewed,reviewedByandreviewedAt, and clearsunreviewedByandunreviewedAt.Undoing sets
reviewedtofalseand recordsunreviewedByandunreviewedAt, while preserving the originalreviewedByandreviewedAtas history.Undoing a file that was never marked, or is already unmarked, is a no-op that returns the current state.
Replacing or deleting a file discards its review state.
Audit trail
Marking a file as reviewed and undoing it produce the audit entries Mark as Verified and Undo Verification — the audit-trail names for those actions — alongside the existing Upload File Attachment and Download File Attachment, all retrievable from GET /aws/rest/services/packages/{packageId}/audit. No classification outcome, extracted value, verification check result or integrity finding produces an audit entry in 26.R5 — if you need a durable record of what validation reported at a point in time, capture it from the verifications endpoint yourself.