Document Intelligence Platform

PowerCred Document Intelligence APIs

Three composable services for lenders and onboarding teams — extract structured data from documents, verify their authenticity, and turn the result into ready-to-consume fraud signals.

Versionv1 Updated12 August 2026 FormatREST / JSON

Introduction

PowerCred Document Intelligence is a three-stage pipeline that takes a raw document (PDF or image) and returns a structured, scored, lender-ready verdict. Each stage is exposed as its own REST API and can be adopted independently.

1
OCR

Convert a payslip, employment certificate, ID, or bank statement into structured JSON fields.

2
Tamper Detection

Verify the source PDF matches a registered template for the document type using metadata, fonts, and layout.

3
Fraud Signals

Run arithmetic, credibility, cross-document and statutory checks. Emit categorised verdicts and lender signals.

Supported Document Types & Services

The table below summarizes the supported document types across the platform and which services are available for each type.

Document Type (document_type) Description OCR Tamper Detection Fraud Signals
payslip Payslips and pay stubs from all employers ✓ Supported ✓ Supported ✓ Supported
employment_certificate Certificate of Employment (COE) / proof of employment ✓ Supported ✓ Supported ✓ Supported
national_id Government national IDs (KTP, UMID, National ID PH, Passport) ✓ Supported ✓ Supported N/A
bank_statement Savings, current, or business bank account statements ✓ Supported ✓ Supported ✓ Supported

Authentication

Each service has its own API key. Keep them separate — they map to different products in our backend.

OCR
?apikey=…

Pass the API key as a apikey query parameter on every request. Do not use the Authorization header.

Tamper Detection
?apikey=…

Pass the API key as a apikey query parameter on every request. Do not use the Authorization header.

Fraud Signals
?apikey=…

Pass the API key as a apikey query parameter on every request. Do not use the Authorization header.

Important: Treat all keys as production secrets even on staging. Never embed them in client-side code or commit them to source control.

Environments

Use staging/dev during integration; promote to production once your verification flow is finalised.

ServiceStaging / DevProduction
OCR https://mock.powercred.io/idp/v1 https://dev.powercred.io/idp/v1
Fraud Signals https://mock.powercred.io/cms/v1 https://dev.powercred.io/cms/v1
Tamper Detection https://mock.powercred.io/idp/fraud https://dev.powercred.io/idp/fraud

End-to-end workflow

A typical loan-onboarding flow chains all three services. The OCR id is the linchpin — once you have it, the same identifier is reused as a session reference by Fraud Signals.

PRIMARY FLOW PARALLEL CHECK LENDER / CLIENT SYSTEM Has a borrower's payslip PDF 1 Submit document POST /idp/v1/read file + document_type Returns: { id } original PDF parallel · optional 2 Verify authenticity POST /check file (original PDF) Returns: { matched, is_epdf, reasons[] } use { id } 3 Fetch structured data GET /idp/v1/get?id=… poll until 200 (or callback) Returns: { data } use { id } or { data } 4 Generate fraud + lender signals OPTION A · session lookup POST /cms/v1/fraud-detection/<type>/run-from-session body: { sessions: [{ session_id: id, product: "idp" }] } OPTION B · inline payload POST /cms/v1/fraud-detection/<type>/run body: { documents: [{ type, data }] } Returns: { run_id, overall_status, categories[], signals, metadata }
Why the two options at step 4? Option A is one round-trip simpler — the service fetches the OCR result for you using the id. Option B lets you score data you already hold (e.g. cached payloads or data extracted by a non-PowerCred OCR). Both produce an identical response shape.

1OCR

Asynchronous structured extraction from PDF, JPEG, and PNG documents. Submit once, poll for the result. Supports payslips, employment certificates, government IDs, and bank statements.

Async by design. Submission returns 202 immediately with an id. Use either polling (GET /get?id=…) or supply callback_url on submit to receive a webhook when extraction is complete.
POST /idp/v1/read Submit a document for extraction

Upload a document (or supply a public file_url) and start extraction. Validation rules are determined by document_type — your account is configured with the schemas you need.

The endpoint accepts two request styles — pick whichever is easier from your caller:

  • Multipart / form-data — use when you're uploading the file bytes directly (file). Other params travel on the query string.
  • JSON body — use when the document is already hosted somewhere accessible (send file_url) or when you'd rather keep everything in the request body. Set Content-Type: application/json.

Option A · Multipart form-data

Params on the query string, file in form-data.

Query parameters
NameTypeRequiredDescription
reference_idstringrequiredYour correlation ID for the end-user or loan application.
document_typeenum (string)requiredDefined document type enum. Supported values: payslip, employment_certificate, national_id, bank_statement, passport, invoice, utility_bill.
file_urlstringoptionalPublic URL of the document. Use instead of multipart file.
callback_urlstringoptionalWebhook to receive the final payload on completion.
tamper_checkbooloptionalRun lightweight inline tamper indicators on the document.
get_image_qualitybooloptionalInclude image quality assessment (blur, brightness, flash, DPI) for camera images.
Form-data
NameTypeRequiredDescription
filebinaryconditionalPDF, JPEG or PNG. Required if file_url is not provided.

Option B · JSON body

Set Content-Type: application/json and pass the Authorization: Bearer <apikey> header. All fields move into the JSON body.

Request body
{
  "reference_id": "ref-12345",
  "document_type": "payslip",
  "file_url": "https://your-storage.example.com/payslip.pdf",
  "callback_url": "https://your-app.example.com/webhooks/ocr",
  "tamper_check": false,
  "get_image_quality": true
}
FieldTypeRequiredDescription
reference_idstringrequiredYour correlation ID for the end-user or loan application.
document_typeenum (string)requiredDefined document type enum (e.g., payslip, employment_certificate, national_id, bank_statement).
file_urlstringrequiredPublic URL of the document (PDF/JPEG/PNG). Since JSON can't carry file bytes, this is the mandatory source in this mode.
callback_urlstringoptionalWebhook to receive the final payload on completion.
tamper_checkbooloptionalRun lightweight inline tamper indicators on the document.
get_image_qualitybooloptionalInclude image quality assessment for camera images.
Body wins ties. If a field is present both on the query string and in the JSON body, the JSON body value is used. Any body field can also be passed as a query parameter — the two forms are interchangeable except for file, which must go through multipart.

Response — 202 Accepted

{
  "message": "Document parsing started",
  "id": "3b41c2e0-9d2a-4e5e-bb37-2b8e0c0fbb12"
}

Common errors

CodeReason
400File is not PDF/JPEG/PNG.
422Missing required field, or document_type not configured on your account.
401Missing or invalid Authorization: Bearer token.
GET /idp/v1/get Retrieve extraction result

Poll with the id returned by /read. The same response shape is returned when the extraction completes via callback.

Request — query parameters

NameTypeRequiredDescription
idstringrequiredThe id returned by POST /read.
return_jsonbooloptionalReturn the raw structured JSON instead of the formatted response.
image_qualitybooloptionalInclude the image quality assessment block when available.

Response — 200 OK (single payslip)

When the submitted file contains a single pay period, data is a JSON object with the extracted fields.

{
  "id": "3b41c2e0-9d2a-4e5e-bb37-2b8e0c0fbb12",
  "document_type": "payslip",
  "status": "COMPLETED",
  "data": {
    "employee_name": "Juan Dela Cruz",
    "employee_id": "EMP-00123",
    "employer_name": "Acme Corporation",
    "pay_period_start": "2026-05-01",
    "pay_period_end": "2026-05-15",
    "pay_date": "2026-05-20",
    "gross_pay": 35000.00,
    "basic_pay": 30000.00,
    "allowances": 5000.00,
    "total_deductions": 4750.00,
    "net_pay": 30250.00,
    "contributions": {
      "sss": 1125.00,
      "philhealth": 875.00,
      "pagibig": 100.00
    },
    "withholding_tax": 2650.00
  }
}

Response — 200 OK (multiple months in one file)

When the submitted file bundles payslips from multiple pay periods (e.g. the last three months in one PDF), data becomes an array — one object per detected payslip, ordered by pay date. All other top-level fields (id, document_type, status) are unchanged. Iterate over data in your client code and treat each element as a full payslip record.

{
  "id": "3b41c2e0-9d2a-4e5e-bb37-2b8e0c0fbb12",
  "document_type": "payslip",
  "status": "COMPLETED",
  "data": [
    {
      "employee_name": "Juan Dela Cruz",
      "employee_id": "EMP-00123",
      "employer_name": "Acme Corporation",
      "pay_period_start": "2026-04-01",
      "pay_period_end": "2026-04-15",
      "pay_date": "2026-04-20",
      "gross_pay": 35000.00,
      "basic_pay": 30000.00,
      "allowances": 5000.00,
      "total_deductions": 4750.00,
      "net_pay": 30250.00,
      "contributions": {"sss": 1125.00, "philhealth": 875.00, "pagibig": 100.00},
      "withholding_tax": 2650.00
    },
    {
      "employee_name": "Juan Dela Cruz",
      "employee_id": "EMP-00123",
      "employer_name": "Acme Corporation",
      "pay_period_start": "2026-05-01",
      "pay_period_end": "2026-05-15",
      "pay_date": "2026-05-20",
      "gross_pay": 35000.00,
      "basic_pay": 30000.00,
      "allowances": 5000.00,
      "total_deductions": 4750.00,
      "net_pay": 30250.00,
      "contributions": {"sss": 1125.00, "philhealth": 875.00, "pagibig": 100.00},
      "withholding_tax": 2650.00
    },
    {
      "employee_name": "Juan Dela Cruz",
      "employee_id": "EMP-00123",
      "employer_name": "Acme Corporation",
      "pay_period_start": "2026-06-01",
      "pay_period_end": "2026-06-15",
      "pay_date": "2026-06-20",
      "gross_pay": 36500.00,
      "basic_pay": 30000.00,
      "allowances": 6500.00,
      "total_deductions": 4900.00,
      "net_pay": 31600.00,
      "contributions": {"sss": 1125.00, "philhealth": 875.00, "pagibig": 100.00},
      "withholding_tax": 2800.00
    }
  ]
}
Feeding this into Fraud Signals. Pass the data array straight through to POST /cms/v1/fraud-detection/{document_type}/run — wrap each element as { "type": "{document_type}", "data": <element> } in the documents[] array. Cross-document checks (employer consistency, pay period continuity, income stability) will then engage automatically.

Status semantics

CodeMeaningAction
200Extraction complete; payload returned.Consume data.
202Extraction still in progress.Retry after 2–3 seconds.
404Document was unreadable.Resubmit a clearer copy.
500Extraction failed.Resubmit; if persistent, contact support.
DELETE /idp/v1/delete Delete an OCR record

Permanently remove an OCR record and its stored extraction data. Useful for right-to-be-forgotten flows or purging test data. The delete is scoped to the account associated with your API key — records belonging to other customers cannot be deleted.

Request — query parameters

NameTypeRequiredDescription
idstringrequiredThe OCR record ID returned by POST /read.

Response — 200 OK

{
  "message": "Data deleted successfully",
  "id": "3b41c2e0-9d2a-4e5e-bb37-2b8e0c0fbb12"
}

Response fields

FieldTypeDescription
messagestringHuman-readable confirmation.
idstringEcho of the deleted record's ID.

Common errors

CodeReason
422Missing id.
401Missing or invalid Authorization: Bearer token.
500Deletion failed. Safe to retry.
Irreversible. Once deleted, the record and its extracted payload can no longer be retrieved via GET /get. If a downstream Fraud Signals run has already been executed against this ID, that run's result is preserved separately and is unaffected.

2Document Tamper Detection

Register reference PDFs for each document type your organisation accepts. Incoming PDFs are then verified against the stored metadata fingerprint, font set, and layout signature. Doctored or fabricated ePDFs are caught before they reach scoring.

How it works

You upload one or more clean reference PDFs per document type (e.g. an employer's payslip template). The service extracts a fingerprint composed of PDF metadata patterns, embedded fonts, and a layout signature, and stores it as the active version. Every incoming PDF is checked against the stored fingerprint, with regex-based pattern matching for metadata and a cosine similarity score for layout.

Auth reminder. Tamper Detection endpoints do not use Bearer headers (No Auth). Pass your API key via query parameter apikey=<key> (e.g. ?apikey=<key>) on every request.
File input — two ways. All four file-taking endpoints (POST /check, POST /templates/{id}/auto, POST /templates/{id}, PUT /templates/{id}) accept either:
  • multipart/form-data — upload raw PDF bytes in the file / files field.
  • application/json — send file_url (single) or file_urls (list). The server downloads each PDF (max 5 MB each, PDF only) and processes it identically.
If a request supplies both, the multipart file(s) win and file_url(s) is ignored.
GET /documents List registered document types

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Response fields

FieldTypeDescription
documentsstring[]All document type identifiers registered under your account.
countintegerLength of documents.

Example — 200 OK

{
  "documents": ["payslip", "national_id"],
  "count": 2
}
POST /templates/{document_id}/auto Auto-extract a template

Upload reference PDFs and let the service derive every validation rule automatically. Metadata patterns and font lists are taken directly from the PDFs:

  • Single unique value across all files → exact-match regex.
  • Multiple differing values → regex alternation (?:a|b|…).
  • Field absent in all files → omitted (not checked).

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Path parameters

NameTypeDescription
document_idstringThe document type identifier you want to register (e.g. payslip).

Option A · multipart/form-data

NameTypeRequiredDescription
filesbinary[]requiredOne or more PDFs, ≤ 5 MB each.
similarity_thresholdfloatoptionalLayout cosine-similarity threshold, default 0.85.

Option B · application/json

{
  "file_urls": [
    "https://your-storage.example.com/payslip_v1.pdf",
    "https://your-storage.example.com/payslip_v2.pdf"
  ],
  "similarity_threshold": 0.85
}
FieldTypeRequiredDescription
file_urlsstring[]requiredPublic URLs of the PDF templates. Each downloaded server-side; same 5 MB and PDF-only rules apply.
similarity_thresholdfloatoptionalLayout cosine-similarity threshold, default 0.85.

Response fields

FieldTypeDescription
version_idstringIdentifier of the newly created version (e.g. v1). This version becomes active immediately.
document_idstringEcho of the path parameter.
template_countintegerNumber of PDFs stored in the new version.
extracted_configobjectConfiguration derived from the uploaded PDFs.
extracted_config.metadata_configobject<string,string>Regex patterns per PDF metadata field (producer, title, author, creator, subject, keywords, creation_date). Fields absent from all uploads are omitted.
extracted_config.fontsstring[]Union of font family names found across the uploaded PDFs.
extracted_config.similarity_thresholdfloatLayout cosine-similarity threshold stored on the version.

Example — 201 Created

{
  "version_id": "v1",
  "document_id": "payslip",
  "template_count": 2,
  "extracted_config": {
    "metadata_config": {
      "producer": "Adobe\\ PDF\\ Library\\ 15\\.0",
      "creator": "Microsoft\\ Word"
    },
    "fonts": ["Arial", "TimesNewRoman"],
    "similarity_threshold": 0.85
  }
}
POST /templates/{document_id} Create a template with explicit rules

Use this when you want full control over the regex patterns and font list, rather than relying on auto-extraction.

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Option A · multipart/form-data

NameTypeRequiredDescription
filesbinary[]requiredOne or more PDFs, ≤ 5 MB each.
producer_patternstringoptionalRegex matched against the PDF producer field.
title_patternstringoptionalRegex for PDF title metadata.
author_patternstringoptionalRegex for PDF author metadata.
creator_patternstringoptionalRegex for PDF creator metadata.
subject_patternstringoptionalRegex for PDF subject metadata.
keywords_patternstringoptionalRegex for PDF keywords metadata.
creation_date_patternstringoptionalRegex for PDF creation date.
fontsstringoptionalJSON array of font families, e.g. ["Arial","Helvetica"].
similarity_thresholdfloatoptionalDefault 0.85.

Option B · application/json

{
  "file_urls": [
    "https://your-storage.example.com/payslip.pdf"
  ],
  "producer_pattern": "Adobe PDF Library",
  "fonts": ["Arial", "Helvetica"],
  "similarity_threshold": 0.85
}
FieldTypeRequiredDescription
file_urlsstring[]requiredPublic URLs of the PDF templates (downloaded server-side).
producer_patternstringoptionalRegex matched against the PDF producer field.
title_patternstringoptionalRegex for PDF title metadata.
author_patternstringoptionalRegex for PDF author metadata.
creator_patternstringoptionalRegex for PDF creator metadata.
subject_patternstringoptionalRegex for PDF subject metadata.
keywords_patternstringoptionalRegex for PDF keywords metadata.
creation_date_patternstringoptionalRegex for PDF creation date.
fontsstring[]optionalNative JSON array of font family names (no stringification needed).
similarity_thresholdfloatoptionalDefault 0.85.

Response fields

FieldTypeDescription
version_idstringNewly created version. Becomes active immediately.
document_idstringEcho of the path parameter.
template_countintegerNumber of PDFs stored in the new version.

Example — 201 Created

{
  "version_id": "v1",
  "document_id": "payslip",
  "template_count": 1
}
PUT /templates/{document_id} Update template configuration

Any field omitted is carried over from the currently active version. A new version is always created and becomes active immediately.

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Option A · multipart/form-data (all optional)

NameTypeDescription
filesbinary[]Additional PDFs.
file_modestringappend (default) keeps existing PDFs; overwrite replaces them with the newly supplied set.
producer_patternstringNew regex matched against PDF producer field.
title_patternstringNew regex for PDF title metadata.
author_patternstringNew regex for PDF author metadata.
creator_patternstringNew regex for PDF creator metadata.
subject_patternstringNew regex for PDF subject metadata.
keywords_patternstringNew regex for PDF keywords metadata.
creation_date_patternstringNew regex for PDF creation date.
fontsstringJSON array of font families.
similarity_thresholdfloatUpdated threshold.

Option B · application/json (all optional)

{
  "file_urls": ["https://your-storage.example.com/payslip_v2.pdf"],
  "producer_pattern": "Adobe PDF Library",
  "fonts": ["Arial", "Helvetica"],
  "similarity_threshold": 0.9,
  "file_mode": "append"
}
FieldTypeDescription
file_urlsstring[]Additional PDFs to add, downloaded server-side.
file_modestringappend (default) or overwrite. In overwrite mode, only file_urls are kept.
producer_patternstringNew regex matched against PDF producer field.
title_patternstringNew regex for PDF title metadata.
author_patternstringNew regex for PDF author metadata.
creator_patternstringNew regex for PDF creator metadata.
subject_patternstringNew regex for PDF subject metadata.
keywords_patternstringNew regex for PDF keywords metadata.
creation_date_patternstringNew regex for PDF creation date.
fontsstring[]Native JSON array of font family names.
similarity_thresholdfloatUpdated threshold.

Response fields

FieldTypeDescription
version_idstringThe new version created by this update. Becomes active immediately.
document_idstringEcho of the path parameter.
template_countintegerTotal PDFs in the new version. With file_mode=append this equals existing + newly uploaded; with overwrite it equals only the newly uploaded set.

Example — 200 OK

{
  "version_id": "v2",
  "document_id": "payslip",
  "template_count": 3
}
GET /templates/{document_id} Get active template

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Response fields

FieldTypeDescription
document_idstringDocument type identifier.
active_versionstringThe currently active version for this document type.
metadata_configobject<string,string>Regex patterns evaluated against the PDF's metadata fields. Keys: producer, title, author, creator, subject, keywords, creation_date. Only keys with rules are present.
fontsstring[]Font families that must appear in any matching PDF.
similarity_thresholdfloatLayout cosine-similarity minimum (0.0–1.0).
template_countintegerReference PDFs stored in the active version.
created_atstring (ISO-8601)When the active version was created.

Example — 200 OK

{
  "document_id": "payslip",
  "active_version": "v1",
  "metadata_config": {"producer": "Adobe PDF Library"},
  "fonts": ["Arial", "Helvetica"],
  "similarity_threshold": 0.85,
  "template_count": 2,
  "created_at": "2026-05-20T08:14:11Z"
}
GET /templates/{document_id}/versions List versions

Returns every version ever created for this document type, ordered by version number. Exactly one will have is_active: true.

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Response fields

FieldTypeDescription
document_idstringDocument type identifier.
versionsobject[]One entry per version.
versions[].version_idstringStable identifier (e.g. v1).
versions[].version_numberintegerMonotonic counter, useful for ordering.
versions[].created_atstring (ISO-8601)Creation timestamp.
versions[].is_activebooleantrue for the version currently used by /check.
versions[].metadata_configobject<string,string>Regex patterns stored on this version.
versions[].fontsstring[]Required fonts on this version.
versions[].similarity_thresholdfloatLayout threshold on this version.
versions[].template_countintegerReference PDFs in this version.

Example — 200 OK

{
  "document_id": "payslip",
  "versions": [
    {
      "version_id": "v1",
      "version_number": 1,
      "created_at": "2026-05-20T08:14:11Z",
      "is_active": false,
      "metadata_config": {"producer": "Adobe PDF Library"},
      "fonts": ["Arial"],
      "similarity_threshold": 0.85,
      "template_count": 2
    },
    {
      "version_id": "v2",
      "version_number": 2,
      "created_at": "2026-06-04T11:02:55Z",
      "is_active": true,
      "metadata_config": {"producer": "Adobe PDF Library", "creator": "Microsoft Word"},
      "fonts": ["Arial", "Helvetica"],
      "similarity_threshold": 0.88,
      "template_count": 3
    }
  ]
}
PATCH /templates/{document_id}/versions/{version_id}/activate Activate a version

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Response fields

FieldTypeDescription
messagestringHuman-readable confirmation.
document_idstringDocument type identifier.
active_versionstringThe version that is now active.

Example — 200 OK

{
  "message": "Version 'v2' is now active",
  "document_id": "payslip",
  "active_version": "v2"
}
DELETE /templates/{document_id}/files/{filename} Remove a reference PDF

Removes one PDF from the active version. A new version is created without that file and is set active.

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).

Response fields

FieldTypeDescription
version_idstringThe new version created without the deleted file. Becomes active immediately.
document_idstringDocument type identifier.
template_countintegerRemaining reference PDFs in the new version.

Example — 200 OK

{
  "version_id": "v3",
  "document_id": "payslip",
  "template_count": 1
}
POST /check Verify a PDF

Run the authenticity checks. By default the PDF is checked against every registered document type; pass ?document_id=… to scope to a single type.

Query parameters

NameTypeRequiredDescription
apikeystringrequiredAPI key passed as a query parameter (Set Auth to No Auth in headers).
document_idstringoptionalCheck against this document type only.

Option A · multipart/form-data

NameTypeRequiredDescription
filebinaryrequiredThe PDF to verify (≤ 5 MB).

Option B · application/json

{
  "file_url": "https://your-storage.example.com/payslip.pdf",
  "document_id": "payslip"
}
FieldTypeRequiredDescription
file_urlstringrequiredPublic URL of the PDF to verify. Downloaded server-side; same 5 MB and PDF-only rules apply.
document_idstringoptionalScope the check to a single document type. Overrides the query parameter of the same name if both are supplied.

Response fields

FieldTypeDescription
matchedbooleanTop-level verdict — true if the PDF matched at least one registered document type.
is_epdfbooleantrue for digitally-generated PDFs, false for scanned/image-only PDFs. When false, no checks run.
messagestring · nullableOptional human-readable note (e.g. why checks were skipped).
resultsobject[]One entry per document type checked. Empty array for scanned PDFs.
results[].document_idstringThe document type checked against.
results[].matchedbooleanOverall match against this document type's active version.
results[].version_checkedstringVersion ID used for the check.
results[].reasons.metadata.matchedbooleanOverall metadata check result.
results[].reasons.metadata.details[]object[]Per-field results: field (string), pattern (regex), extracted_value (string·nullable), matched (boolean).
results[].reasons.fonts.matchedbooleanWhether every required font was present.
results[].reasons.fonts.details[]object[]Per-font results: required_font (string), found (boolean), extracted_fonts (string[]).
results[].reasons.layout.matchedbooleanWhether layout similarity met the threshold.
results[].reasons.layout.scorefloatCosine similarity against the closest reference (0.0–1.0).
results[].reasons.layout.thresholdfloatThreshold value used for the comparison.
results[].reasons.date_integrity.matchedbooleantrue when modification date is consistent with creation date.
results[].reasons.date_integrity.creation_datestring · nullablePDF creation timestamp, if present.
results[].reasons.date_integrity.mod_datestring · nullablePDF last-modified timestamp, if present.

Example — 200 OK (ePDF, matched)

{
  "matched": true,
  "is_epdf": true,
  "results": [
    {
      "document_id": "payslip",
      "matched": true,
      "version_checked": "v1",
      "reasons": {
        "metadata": {
          "matched": true,
          "details": [
            {
              "field": "producer",
              "pattern": "Adobe PDF Library",
              "extracted_value": "Adobe PDF Library 15.0",
              "matched": true
            }
          ]
        },
        "fonts": {
          "matched": true,
          "details": [
            {"required_font": "Arial", "found": true, "extracted_fonts": ["Arial", "Helvetica"]}
          ]
        },
        "layout": {"matched": true, "score": 0.94, "threshold": 0.85},
        "date_integrity": {"matched": true, "creation_date": "2026-05-15T10:22:01Z", "mod_date": "2026-05-15T10:22:01Z"}
      }
    }
  ]
}

Response — 200 OK (scanned/image PDF)

{
  "matched": false,
  "is_epdf": false,
  "message": "PDF appears to be scanned/image-based; tamper checks skipped.",
  "results": []
}
Reading the result. A top-level matched: true means the PDF matched at least one registered document type. The per-document reasons object explains which sub-checks passed or failed — useful for surfacing actionable feedback to operators.

3Fraud Signals

Take structured document data and emit categorised fraud verdicts plus lender-ready financial signals. Currently supports payslips and employment certificates.

What's evaluated

CategoryWhat it checks
Arithmetic IntegrityNet pay reconciliation, cross-field consistency, income stability across periods.
Document CredibilityTrust score, format reliability, employment formality, pay frequency cadence.
Cross-Document ChecksEmployer consistency and pay period continuity when multiple documents are submitted.
Statutory ComplianceRegion-specific contribution and tax compliance (SSS, PhilHealth, Pag-IBIG, withholding, minimum wage for region=ph).
POST /cms/v1/fraud-detection/{document_type}/run Evaluate from JSON payload

Use this when you already hold the structured document data (typically the response body from OCR).

Path parameters

NameTypeDescription
document_typestringpayslip or employment_certificate.

Query parameters

NameTypeRequiredDescription
regionstringoptionalRegion code for statutory rules. Defaults to ph.

Request body

{
  "documents": [
    {
      "type": "payslip",
      "document_id": "payslip_1",
      "data": { /* extracted JSON, typically from OCR */ }
    }
  ]
}
FieldTypeRequiredDescription
documentsarrayrequiredAt least one document. Every item's type must match the path.
documents[].typestringrequiredDocument type; must equal {document_type}.
documents[].document_idstringoptionalYour identifier. Defaults to doc_0, doc_1, …
documents[].dataobjectrequiredExtracted structured data for the document.

Response fields

All three Fraud Signals endpoints (/run, /run-from-session, GET /runs/{run_id}) return the same FraudDetectionResponse shape documented below. For the full enumeration of payslip checks, per-status examples, and HTTP error responses, see Possible responses — payslip.

FieldTypeDescription
run_idstring (UUID)Unique identifier for this evaluation. Persist this — you can refetch the result later with GET /runs/{run_id}.
document_typestringpayslip or employment_certificate. Echoes the path parameter.
timestampstring (ISO-8601)When the run was executed (UTC).
overall_statusenumWorst status across all categories: pass, warn, fail, or inconclusive.
categories[]object[]One entry per category evaluated. See Category below.
signalsobjectLender-ready summary derived from the document. See Signals below.
gcsobject<string,string>Internal storage references for the audit trail; safe to ignore in client code.
metadataobjectRequest context and processing details. See Metadata below.
session_dataobject[] · nullablePresent only for /run-from-session. Lists the upstream sessions resolved: session_id, product, document_type.

Category

FieldTypeDescription
namestringCategory label (e.g. Arithmetic Integrity, Document Credibility, Cross-Document Checks, Statutory Compliance).
descriptionstringWhat this category evaluates.
statusenumWorst status across this category's checks.
checks[]object[]Individual checks. See Check below.

Check

FieldTypeDescription
namestringStable check identifier (e.g. net_pay_reconciliation).
descriptionstringPlain-English explanation.
statusenumpass, warn, fail, or inconclusive.
checks_performedstring[]Sub-checks executed within this check.
findings[]object[]Issues raised by the check. Each finding has: severity (high/medium/low), code (stable identifier), message (description), evidence (object with relevant raw values).

Signals

FieldTypeDescription
signals.financial_summaryobjectNormalised financial figures pulled from the document. Each field is nullable.
financial_summary.gross_paynumberGross compensation for the period.
financial_summary.net_paynumberTake-home amount.
financial_summary.basic_paynumberBase salary component.
financial_summary.total_deductionsnumberSum of all deductions.
financial_summary.sss_contributionnumberSSS contribution (PH).
financial_summary.philhealth_contributionnumberPhilHealth contribution (PH).
financial_summary.pagibig_contributionnumberPag-IBIG contribution (PH).
financial_summary.withholding_taxnumberWithholding tax for the period.
financial_summary.takehome_rationumbernet_pay / gross_pay — useful for affordability scoring.
signals.lender_signals[]object[]Cards summarising the document for underwriting. Each entry has: name (identifier), value (any), status (pass/warn/fail/inconclusive), evidence (object · nullable).

Metadata

FieldTypeDescription
metadata.app_idstringYour application ID.
metadata.developer_emailstringDeveloper email tied to the API key.
metadata.app_namestringYour application name.
metadata.document_typestringEcho of the path parameter.
metadata.processing_time_msnumberEnd-to-end processing time in milliseconds.
metadata.request_ipstring · nullableOriginating IP, if available.
metadata.user_agentstring · nullableOriginating user agent, if available.

Example — 200 OK

{
  "run_id": "3f8c…",
  "document_type": "payslip",
  "timestamp": "2026-06-16T09:12:33Z",
  "overall_status": "pass",
  "categories": [
    {
      "name": "Arithmetic Integrity",
      "description": "Net pay calculations, cross-field consistency, and income stability",
      "status": "pass",
      "checks": [
        {
          "name": "net_pay_reconciliation",
          "description": "gross - deductions = net",
          "status": "pass",
          "checks_performed": ["sum_deductions", "net_pay_match"],
          "findings": []
        }
      ]
    },
    { "name": "Document Credibility",   "status": "pass", "checks": [] },
    { "name": "Cross-Document Checks",  "status": "pass", "checks": [] },
    { "name": "Statutory Compliance",   "status": "pass", "checks": [] }
  ],
  "signals": {
    "financial_summary": {
      "gross_pay": 35000.00,
      "net_pay": 30250.00,
      "basic_pay": 30000.00,
      "total_deductions": 4750.00,
      "sss_contribution": 1125.00,
      "philhealth_contribution": 875.00,
      "pagibig_contribution": 100.00,
      "withholding_tax": 2650.00,
      "takehome_ratio": 0.864
    },
    "lender_signals": [
      { "name": "income_stability",   "value": "stable",   "status": "pass" },
      { "name": "statutory_coverage", "value": "complete", "status": "pass" }
    ]
  },
  "metadata": {
    "app_id": "app_xxx",
    "developer_email": "dev@example.com",
    "app_name": "my-app",
    "document_type": "payslip",
    "processing_time_ms": 412.0
  }
}

Overall status values

StatusMeaning
passAll checks passed; document looks legitimate.
warnOne or more soft anomalies; review recommended.
failAt least one hard rule failed; treat as suspicious.
inconclusiveNot enough information to decide.
POST /cms/v1/fraud-detection/{document_type}/run-from-session Evaluate from an upstream session

Skip uploading data — point at an existing PowerCred session (e.g. the OCR id) and we fetch the document data for you.

Path parameters

NameTypeDescription
document_typestringpayslip or employment_certificate.

Query parameters

NameTypeRequiredDescription
regionstringoptionalRegion code for statutory rules. Defaults to ph.

Request body

{
  "sessions": [
    { "session_id": "3b41c2e0-9d2a-4e5e-bb37-2b8e0c0fbb12", "product": "idp" }
  ]
}
FieldTypeRequiredDescription
sessionsarrayrequiredAt least one upstream session reference.
sessions[].session_idstringrequiredThe upstream identifier — typically the OCR id.
sessions[].productstringrequiredThe upstream product key. Errors include the list of supported values.

Response fields

Same FraudDetectionResponse shape as POST /run, with the session_data field populated.

FieldTypeDescription
session_dataobject[]One entry per resolved upstream session.
session_data[].session_idstringThe session ID supplied in the request.
session_data[].productstringThe upstream product key (e.g. idp).
session_data[].document_typestringDocument type of the fetched record. Must match the path parameter.

All other fields (run_id, overall_status, categories, signals, metadata) are identical to the /run response.

Example — 200 OK

{
  "run_id": "3f8c…",
  "document_type": "payslip",
  "timestamp": "2026-06-16T09:14:02Z",
  "overall_status": "pass",
  "session_data": [
    {
      "session_id": "3b41c2e0-9d2a-4e5e-bb37-2b8e0c0fbb12",
      "product": "idp",
      "document_type": "payslip"
    }
  ],
  "categories": [ /* same shape as /run */ ],
  "signals":    { /* same shape as /run */ },
  "metadata":   { /* same shape as /run */ }
}
GET /cms/v1/fraud-detection/runs/{run_id} Fetch a past run

Refetch a previously executed run by run_id.

Path parameters

NameTypeDescription
run_idstring (UUID)The run_id returned by a previous /run or /run-from-session call under the same account.

Response fields

Returns the same FraudDetectionResponse shape as POST /run. If the original run was session-based, session_data will be populated as well.

Errors

CodeReason
404No run with that run_id exists under your account.
Possible responses — payslip Checks catalog, per-status examples, and errors

Every payslip check the pipeline can emit, one worked example body per overall_status, and the HTTP error responses shared by all three Fraud Signals endpoints. Applies to document_type = payslip with region = ph; other regions and document types have their own catalogs.

Payslip check catalog

Each check surfaces under its parent category with a stable name, a status, and — when a rule fires — one or more findings. Cross-document checks are only meaningful when two or more payslips are submitted in the same request; with a single payslip they resolve to inconclusive, which is not a negative signal. Finding code values are stable identifiers; new codes may be added as the pipeline evolves, so treat the illustrative codes in the examples below as representative rather than exhaustive.

CategoryCheck nameWhat it evaluatesPossible outcomes
Arithmetic Integrity net_pay_reconciliation gross_pay − total_deductions = net_pay. pass when the identity holds · fail on mismatch · inconclusive if any of the three figures is missing.
cross_field_consistency Individual deductions sum to the stated total_deductions; related figures agree with each other. pass · warn or fail depending on how many sub-checks disagree.
takehome_ratio net_pay / gross_pay. Plausible band: roughly 0.75 – 0.92. pass inside band · warn just outside · fail at extremes (≤ 0 or > 1).
minimum_wage Declared income (annualised estimate) is above the applicable regional minimum wage. pass · fail below minimum · inconclusive if the region's table is unavailable.
deduction_ratio total_deductions / gross_pay. Plausible band: roughly 0.08 – 0.25. pass inside band · warn outside · fail for negative or > 1 values.
Document Credibility employment_formality_score 0–100 score based on presence of SSS number, PhilHealth number, Pag-IBIG number, employee ID, employer name, and employer address. pass · warn when score < 50 · fail when score is very low.
income_regularity Detected pay cycle from pay-period dates: monthly, biweekly, semi_monthly, or unknown. pass for a recognised cycle · warn when unknown · inconclusive if dates are missing.
document_trust_score Composite 0–100 combining formality, arithmetic integrity, and statutory coverage — the headline credibility number. pass · warn when score < 60 · fail when score is very low.
Cross-Document Checks employer_consistency Fuzzy match of employer_name across all submitted payslips. pass · warn for minor formatting differences · fail for distinct employers · inconclusive with a single payslip.
pay_period_sequence Pay periods are chronological, non-overlapping, and free of unexplained gaps. pass · warn for gaps · fail for overlaps or reversed order · inconclusive with a single payslip.
Statutory Compliance (PH) sss_contribution Declared SSS deduction matches the salary-bracket table for the stated gross_pay. pass · warn for off-by-one-bracket · fail for larger deviations · inconclusive if SSS is absent.
philhealth_contribution Declared PhilHealth deduction matches the fixed percentage of basic pay, subject to the monthly cap. pass · warn for small deviations · fail otherwise · inconclusive if absent.
pagibig_contribution Declared Pag-IBIG deduction follows the 1% / 2% tier, capped at ₱100 per month. pass · warn / fail based on deviation · inconclusive if absent.
mandatory_deductions_coverage All three mandatory deductions (SSS, PhilHealth, Pag-IBIG) are present on the payslip. pass at 3 / 3 · warn at 2 / 3 · fail at 1 / 3 or 0 / 3.

Lender signals

The signals.lender_signals[] array carries up to ten cards summarising the payslip for underwriting. Each entry is { name, value, status, evidence? }.

Signal nameValue shapeInterpretation
payslip_arithmetic_integrity_scorenumber (0–100)Below 80 indicates meaningful arithmetic inconsistencies.
document_trust_scorenumber (0–100)Below 60 warrants manual review.
employment_formality_scorenumber (0–100)Below 50 indicates a low-formality or informal employment arrangement.
sss_contribution_consistency_scorenumber (0–100)How closely the SSS deduction matches the statutory bracket.
mandatory_deductions_coverage_ratiostring, e.g. "3/3"1/3 or 0/3 is a strong red flag.
net_income_stability_proxynumber (ratio)Take-home percentage of gross pay. Normal range ~0.75–0.92.
cross_field_consistencystring, e.g. "5/5 checks passed"How many internal numeric cross-checks passed.
minimum_wage_checkstring ("above" / "below")below means the stated salary is legally implausible.
deduction_ratio_checkstring ("normal" / "low" / "high")Typical range is 8%–25% for a formally employed PH worker.
income_regularitystring ("monthly", "biweekly", "semi_monthly", "unknown")Detected pay cycle; unknown means dates could not be resolved.

Example — 200 OK · overall_status: "pass"

Single payslip; everything balances and every mandatory deduction is present. Cross-document checks resolve to inconclusive (single document) but do not affect the overall verdict because they are not evaluated in the aggregate.

{
  "run_id": "3f8c1e12-9c0a-4c17-a1b6-2b8e0c0fbb12",
  "document_type": "payslip",
  "overall_status": "pass",
  "categories": [
    { "name": "Arithmetic Integrity",  "status": "pass", "checks": [ /* … */ ] },
    { "name": "Document Credibility",  "status": "pass", "checks": [ /* … */ ] },
    { "name": "Cross-Document Checks", "status": "inconclusive", "checks": [ /* single-doc */ ] },
    { "name": "Statutory Compliance",  "status": "pass", "checks": [ /* … */ ] }
  ],
  "signals": {
    "financial_summary": {
      "gross_pay": 35000.00, "net_pay": 30250.00, "total_deductions": 4750.00,
      "sss_contribution": 1125.00, "philhealth_contribution": 875.00, "pagibig_contribution": 100.00,
      "withholding_tax": 2650.00, "takehome_ratio": 0.864
    },
    "lender_signals": [
      { "name": "payslip_arithmetic_integrity_score", "value": 100,   "status": "pass" },
      { "name": "document_trust_score",               "value": 92,    "status": "pass" },
      { "name": "employment_formality_score",         "value": 100,   "status": "pass" },
      { "name": "mandatory_deductions_coverage_ratio","value": "3/3", "status": "pass" }
    ]
  }
}

Example — 200 OK · overall_status: "warn"

Two payslips submitted; arithmetic and statutory checks are clean, but the employer name differs cosmetically between the two documents.

{
  "run_id": "7c22a5b8-4d9f-4a1b-b7ea-9d4d1f83c001",
  "document_type": "payslip",
  "overall_status": "warn",
  "categories": [
    {
      "name": "Cross-Document Checks",
      "status": "warn",
      "checks": [
        {
          "name": "employer_consistency",
          "status": "warn",
          "findings": [
            {
              "severity": "medium",
              "code": "employer_name_fuzzy_mismatch",
              "message": "Employer names differ in formatting across payslips.",
              "evidence": { "values": ["Acme Corp", "ACME Corporation Inc."], "similarity": 0.78 }
            }
          ]
        }
      ]
    }
  ],
  "signals": {
    "lender_signals": [
      { "name": "document_trust_score", "value": 74, "status": "warn" }
    ]
  }
}

Example — 200 OK · overall_status: "fail"

Two hard failures: arithmetic does not balance, and Pag-IBIG is missing from the deductions.

{
  "run_id": "b1035e77-9e21-4c8b-8bbd-2d9f4b7c0e12",
  "document_type": "payslip",
  "overall_status": "fail",
  "categories": [
    {
      "name": "Arithmetic Integrity",
      "status": "fail",
      "checks": [
        {
          "name": "net_pay_reconciliation",
          "status": "fail",
          "findings": [
            {
              "severity": "high",
              "code": "net_pay_mismatch",
              "message": "gross_pay - total_deductions does not equal net_pay.",
              "evidence": {
                "gross_pay": 35000, "total_deductions": 4750,
                "net_pay": 31500, "expected_net_pay": 30250
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Statutory Compliance",
      "status": "fail",
      "checks": [
        {
          "name": "mandatory_deductions_coverage",
          "status": "fail",
          "findings": [
            {
              "severity": "high",
              "code": "pagibig_contribution_missing",
              "message": "Pag-IBIG contribution is not present on the payslip.",
              "evidence": { "present": ["sss", "philhealth"], "missing": ["pagibig"] }
            }
          ]
        }
      ]
    }
  ],
  "signals": {
    "lender_signals": [
      { "name": "payslip_arithmetic_integrity_score", "value": 40,    "status": "fail" },
      { "name": "mandatory_deductions_coverage_ratio","value": "2/3", "status": "fail" }
    ]
  }
}

Example — 200 OK · overall_status: "inconclusive"

Single payslip with key fields missing — the pipeline runs to completion but cannot reach a verdict on multiple categories.

{
  "run_id": "e59f0a3e-1d4c-4e17-b2e5-52ab4c9f1b21",
  "document_type": "payslip",
  "overall_status": "inconclusive",
  "categories": [
    {
      "name": "Cross-Document Checks",
      "status": "inconclusive",
      "checks": [
        {
          "name": "employer_consistency",
          "status": "inconclusive",
          "findings": [
            {
              "severity": "low",
              "code": "single_document",
              "message": "Cross-document checks require two or more payslips.",
              "evidence": { "document_count": 1 }
            }
          ]
        }
      ]
    },
    {
      "name": "Statutory Compliance",
      "status": "inconclusive",
      "checks": [
        {
          "name": "sss_contribution",
          "status": "inconclusive",
          "findings": [
            {
              "severity": "low",
              "code": "missing_field",
              "message": "gross_pay is required to resolve the SSS bracket.",
              "evidence": { "missing": ["gross_pay"] }
            }
          ]
        }
      ]
    }
  ]
}
Reading inconclusive. An inconclusive verdict is not a fraud signal — it means the pipeline could not decide. Typical causes: single-payslip submission (cross-document checks cannot run), missing key fields (SSS number, pay-period dates), or statutory rate tables unavailable for the payslip's year or region. Requesting a more complete or more recent payslip is usually the right response.

HTTP error responses

All three Fraud Signals endpoints share the same error surface. The body shape is { "detail": … } — either a string (validation errors) or an object with error_code and message (upstream and downstream failures).

Document — verify, authenticate, score

Combined flow across all three services. Each call is independent; you can fan out steps 2 and 3 in parallel.

# 1) Submit the document for OCR
curl -X POST "https://mock.powercred.io/idp/v1/read?reference_id=ref-12345&document_type={document_type}" \
  -H "Authorization: Bearer $IDP_KEY" \
  -F "file=@./document.pdf"
# → { "id": "OCR_ID", ... }

# 2) Poll for the structured payload
curl "https://mock.powercred.io/idp/v1/get?id=$OCR_ID" \
  -H "Authorization: Bearer $IDP_KEY"
# → { "status": "COMPLETED", "data": { ... } }

# 3) Verify authenticity (run in parallel with step 2; No Auth header required, use ?apikey=)
curl -X POST "https://mock.powercred.io/idp/fraud/check?apikey=$TAMPER_KEY&document_id={document_type}" \
  -F "file=@./document.pdf"
# → { "matched": true, "is_epdf": true, ... }

# 4) Generate fraud + lender signals from the OCR session
curl -X POST "https://mock.powercred.io/cms/v1/fraud-detection/{document_type}/run-from-session?region=ph" \
  -H "Authorization: Bearer $CMS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sessions":[{"session_id":"'"$OCR_ID"'","product":"idp"}]}'
# → { "overall_status": "pass", "signals": {...}, ... }

Multi-document cross-period check

Use multiple documents in a single fraud-detection call to surface cross-period anomalies (employer mismatch, pay gaps, sudden income jumps).

curl -X POST "https://mock.powercred.io/cms/v1/fraud-detection/{document_type}/run?region=ph" \
  -H "Authorization: Bearer $CMS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "documents": [
      {"type": "{document_type}", "document_id": "p_1", "data": { /* Month 1 */ }},
      {"type": "{document_type}", "document_id": "p_2", "data": { /* Month 2 */ }},
      {"type": "{document_type}", "document_id": "p_3", "data": { /* Month 3 */ }}
    ]
  }'

Tamper template lifecycle

# Initial template — let the service derive everything (No Auth header, use ?apikey=)
curl -X POST "https://mock.powercred.io/idp/fraud/templates/{document_id}/auto?apikey=$TAMPER_KEY" \
  -F "files=@./template_v1.pdf" \
  -F "similarity_threshold=0.85"
# → { "version_id": "v1", ... }

# Update template → upload new reference, append to existing
curl -X PUT "https://mock.powercred.io/idp/fraud/templates/{document_id}?apikey=$TAMPER_KEY" \
  -F "files=@./template_v2.pdf" \
  -F "file_mode=append"
# → { "version_id": "v2", ... }

# Roll back to the previous version
curl -X PATCH "https://mock.powercred.io/idp/fraud/templates/{document_id}/versions/v1/activate?apikey=$TAMPER_KEY"

HTTP status codes

CodeWhenExample body
401 Missing or invalid Authorization: Bearer token. {"detail": "Invalid API key."}
404 GET /runs/{run_id}: no run with that ID under this account. /run-from-session: the upstream session_id was not found for the given product. {"detail": "Run not found."}
422 Body validation failed. Includes: empty documents[], an item whose type disagrees with the path document_type, unknown product on /run-from-session, or the resolved session's document type not matching the path. {"detail": "All documents must be of type 'payslip'. Got: ['employment_certificate']"}
500 Unhandled pipeline error. Safe to retry — runs are idempotent per submission. {"detail": {"error_code": "INTERNAL_ERROR", "message": "Please retry."}}
CodeMeaning
200Successful request. Payload included.
201Resource created (template versions).
202Accepted / processing not yet complete.
400Bad request — usually a malformed file or unsupported MIME type.
401Missing or invalid authentication credential.
404Resource not found.
422Validation failed — missing required field, invalid document type, etc.
500Server error. Safe to retry idempotent calls.

Frequently Asked Questions (FAQ)

Answers to common questions regarding authentication, document types, and pipeline integration.

1. How does authentication work across the Document Intelligence APIs?

Pass your API key as a query parameter (?apikey=<apikey>) on every request across all services (OCR, Tamper Detection, and Fraud Signals). Do not send the Authorization: Bearer header.

2. Is document_type limited to payslips?

No. {document_type} is a path/query parameter across all services. Supported document types include payslip, employment_certificate, national_id, and bank_statement. Each unique document layout should use a distinct document_id when registering templates.

3. How do I link an OCR result to Fraud Signals without re-uploading data?

When you submit a document to POST /idp/v1/read, the server returns a unique job id. Once complete, call POST /cms/v1/fraud-detection/{document_type}/run-from-session with "session_id": "<id>" and "product": "idp". The platform automatically resolves the OCR output internally.

4. What happens when a scanned PDF or photo of a document is submitted to Tamper Detection?

Tamper Detection verifies digital ePDF features (embedded fonts, PDF metadata patterns, and layout grid). If a document contains fewer than 300 characters of extractable text, it is classified as a scanned document. The service returns is_epdf: false and matched: true with an informational message so your pipeline can continue to OCR.

5. How does template versioning work for Tamper Detection?

Every create (`POST`), update (`PUT`), or file delete (`DELETE`) creates a new immutable version (`v1`, `v2`, `v3`). Old versions are retained for audit history. The latest version becomes active automatically, but you can switch active versions anytime via `PATCH /templates/{document_id}/versions/{version_id}/activate`.

6. What are the file size and format limits?

The maximum upload file size is 5 MB per document across all endpoints. OCR accepts PDF, JPEG, and PNG files. Tamper Detection requires vector/text PDF documents.