A Flask API for extracting structured data from identity documents, business-registration documents, financial records, and utility proof-of-address documents. The project supports passport MRZ extraction, Nigerian NIN card/slip parsing, jurisdiction-aware business-document extraction, bank statement parsing, utility bill/receipt parsing, and an optional generic webhook forwarder.
The codebase is organized so new document types and country-specific rules can be added without reshaping the whole service.
- Passport MRZ extraction with TD3 validation and image-quality checks.
- Nigerian NIN card and slip parsing with normalized response fields.
- Bank statement summary extraction from PDFs and images.
- Utility bill and payment receipt extraction with address, receipt date, and month-age calculation.
- Optional webhook forwarding to up to three configured targets.
- Swagger UI at
/api-docs. - HMAC request-signing utilities for production authentication.
- OCR backend abstraction with RapidOCR first and optional EasyOCR fallback.
- Business-document classification, jurisdiction detection, typed identifiers, field-level confidence/evidence, conflict reporting, and generic fallback extraction.
- One shared file-or-URL input layer for every OCR endpoint, with bounded retrieval and SSRF defenses.
- Human-readable, indented JSON for every JSON API response.
document-ocr-api/
app.py
requirements.txt
src/
api/
routes.py
countries/
profile.py
registry.py
ghana/
nigeria/
core/
auth.py
document_source.py
flash_glance.py
ocr_engine.py
document_ocr/
bank_statement/
business_document/
drivers_license/
nin/
passport/
utility_bill/
voter_id/
webhook_forwarder/
broadcast.py
routes.py
signing.py
tests/
- Python 3.11 or 3.12
- pip
- RapidOCR dependencies from
requirements.txt
RapidOCR is the preferred OCR backend. EasyOCR can be enabled as a fallback with ENABLE_EASYOCR_FALLBACK=1, but it is slower.
git clone https://github.com/YOUR_USERNAME/document-ocr-api.git
cd document-ocr-api
python -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
copy .env.example .envOn Linux or macOS, activate the environment with:
source venv/bin/activateCreate a .env file from .env.example.
OCR_SECRET_KEY=change-this-in-production
FORWARDER_SECRET=change-this-if-webhook-forwarding-is-enabled
FORWARDER_TARGET_1_URL=https://endpoint1.example.com/webhook
FORWARDER_TARGET_2_URL=https://endpoint2.example.com/webhook
FORWARDER_TARGET_3_URL=https://endpoint3.example.com/webhook
ENABLE_EASYOCR_FALLBACK=0
DOCUMENT_MAX_UPLOAD_BYTES=20971520
DOCUMENT_INPUT_SPOOL_MEMORY_BYTES=1048576
DOCUMENT_URL_ENABLED=1
DOCUMENT_URL_ALLOW_HTTP=0
DOCUMENT_URL_ALLOWED_PORTS=443
DOCUMENT_URL_ALLOWED_HOSTS=
DOCUMENT_URL_CONNECT_TIMEOUT_SECONDS=3
DOCUMENT_URL_READ_TIMEOUT_SECONDS=8
DOCUMENT_URL_TOTAL_TIMEOUT_SECONDS=20
DOCUMENT_URL_MAX_REDIRECTS=3
DOCUMENT_URL_MAX_LENGTH=2048
BUSINESS_DOCUMENT_MAX_PAGES=20
BUSINESS_DOCUMENT_MAX_UPLOAD_BYTES=20971520
BUSINESS_DOCUMENT_MAX_IMAGE_PIXELS=25000000
BUSINESS_DOCUMENT_MAX_PAGE_TEXT_CHARS=100000
BUSINESS_DOCUMENT_COMPARE_RENDERED_PDF_TEXT=1FORWARDER_* settings are only required if /api/webhooks/forward is used.
DOCUMENT_URL_ALLOWED_HOSTS is an optional comma-separated allowlist. It accepts exact hosts and leading wildcards such as documents.example.com,*.trusted.example. Leave it empty to allow any public destination that passes the network checks. If HTTP is intentionally enabled, also add port 80 to DOCUMENT_URL_ALLOWED_PORTS.
python app.pyThe API runs on http://localhost:5005.
For development on Flask's conventional port 5000, use reload mode so application changes are picked up:
python -m flask --app app --debug run --port 5000If a server was already running before an update, stop it with Ctrl+C and start it again. flask run without --debug keeps the previously imported application in memory, which can make responses appear unchanged after the source has been updated.
Swagger UI is available at:
http://localhost:5005/api-docs
For production-style serving:
gunicorn --bind 0.0.0.0:5005 --workers 4 --timeout 120 app:appEvery OCR endpoint accepts exactly one document source:
- Multipart upload:
file=@document.pdf - Body URL in form data:
url=https://files.example.com/document.pdf - Body URL in JSON:
{"url":"https://files.example.com/document.pdf"}
document_url is accepted as an alias for url. Optional endpoint hints such as country, jurisdiction, and document_type can be included in the same form or JSON body. Do not put document URLs in the query string: signed URLs often contain credentials, and query strings are more likely to be retained in access logs.
Upload example in PowerShell:
curl.exe -X POST "http://localhost:5005/api/business-document" `
-F "file=@C:\path\to\certificate.pdf" `
-F "country=NGA"URL example in PowerShell:
curl.exe -X POST "http://localhost:5005/api/business-document" `
-H "Content-Type: application/json" `
-d '{"url":"https://files.example.com/certificate.pdf","country":"NGA"}'Remote retrieval is enabled for public HTTPS URLs by default. The resolver rejects credentials, fragments, unapproved ports, non-public or mixed public/private DNS answers, redirect loops, HTTPS-to-HTTP downgrades, compressed responses, unsupported file signatures, and responses over the configured byte limit. It revalidates every redirect, connects to a validated address while preserving TLS hostname verification, ignores environment proxy settings, and applies connect/read/total timeouts. Remote endpoints that require cookies, authorization headers, or interactive login are not supported.
Passport, legacy passport scan, and NIN routes accept images only. The other OCR routes accept PDFs and supported images (JPEG, PNG, TIFF, BMP, and WebP). File type is determined from content bytes, not a filename or remote Content-Type claim.
All responses produced as JSON—including errors and legacy endpoint responses—are indented for readability. Clients should still parse JSON rather than depend on whitespace.
GET /Returns:
{
"status": "healthy",
"message": "Document OCR API is live"
}POST /api/passport
POST /api/scan-passportForm data:
fileorurl: passport image sourcecountry(optional): ISO-3166 alpha-3 country hint, for exampleNGA
Example:
curl -X POST http://localhost:5005/api/passport ^
-F "file=@passport.jpg"POST /api/ninForm data:
fileorurl: NIN card or slip image sourcecountry(optional): ISO-3166 alpha-3 country code. Defaults toNGA.
POST /api/bank-statementForm data:
fileorurl: PDF or image bank statement source
POST /api/utility-billForm data:
fileorurl: utility bill or utility payment receipt image/PDF sourcecountry(optional): ISO-3166 alpha-3 country code. Defaults toNGA.
The utility bill response focuses on proof-of-address checks. It returns the
service address, receipt/bill date, days_old, months_old, and an is_recent
flag based on a 90-day freshness window. Older receipts can still return
success: true when the address and date are readable; consumers can decide how
to enforce freshness from is_recent.
Example:
curl -X POST http://localhost:5005/api/utility-bill ^
-F "file=@utility_receipt.jpg" ^
-F "country=NGA"POST /api/voter-idForm data:
fileorurl: voter document image, or PDF with embedded textcountry(optional): ISO-3166 alpha-3 country code. Defaults toNGA.
voter_id is the canonical processor name. Country metadata keeps local naming
clear: Nigeria exposes VOTER_CARD, while Ghana exposes VOTER_ID.
POST /api/drivers-licenseForm data:
fileorurl: driver's license image, or PDF with embedded textcountry(optional): ISO-3166 alpha-3 country code. Defaults toNGA.
For these newer identity processors, the runtime flow is:
Flask route -> shared document processor -> text_extraction.py -> country parser
For example, /api/voter-id calls src/document_ocr/voter_id/processor.py.
That processor calls src/document_ocr/text_extraction.py to convert the upload
into text, then dispatches to src/countries/nigeria/voter_id.py or
src/countries/ghana/voter_id.py.
POST /api/business-documentForm data:
fileorurl: PDF, JPEG, PNG, TIFF, BMP, or WebP business document sourcecountry(optional): country code or registered country alias, for exampleNGAorUSAjurisdiction(optional): state, province, or other subnational jurisdiction hintdocument_type(optional): taxonomy code, for exampleCERTIFICATE_OF_INCORPORATIONresponse_detail(optional):summary(default) orfull
The default summary response removes empty fields, repeated role projections, duplicate confidence bands, classification alternatives, and verbose page/candidate diagnostics. It retains typed registration/tax identifiers, concise confidence and evidence, warnings/conflicts, extraction totals, raw OCR text, and unclassified fields. Nigeria and United States profiles are built in; unknown jurisdictions use the generic fallback.
curl -X POST http://localhost:5005/api/business-document \
-F "file=@certificate.pdf" \
-F "country=NGA"Request the complete audit/debug representation when every candidate, alternative, page diagnostic, and derived role list is needed:
curl -X POST http://localhost:5005/api/business-document \
-F "file=@certificate.pdf" \
-F "response_detail=full"See Business-document OCR for the complete response contract, supported taxonomy, profile-extension example, limits, privacy guidance, and known limitations.
GET /api/countries
GET /api/countries/{country_code}Returns registered countries and their local identity document metadata. This is metadata only; a listed ID does not automatically mean an OCR parser exists for that exact ID yet.
Example:
curl http://localhost:5005/api/countries/NGAExample response fragment:
{
"success": true,
"country": {
"country_code": "NGA",
"country_name": "Nigeria",
"supported_identity_documents": [
{"code": "NIN_CARD", "name": "National Identification Number card"},
{"code": "VOTER_CARD", "name": "Permanent voter card"},
{"code": "DRIVERS_LICENSE", "name": "Driver's license"}
]
}
}POST /api/webhooks/forwardReceives a raw request body, signs it with FORWARDER_SECRET, and forwards it to configured targets.
Forwarded requests include:
X-TimestampX-SignatureX-Source: webhook-forwarderContent-Type, when provided by the original requestX-Request-IdorX-Correlation-Id, when provided
The forwarder keeps a short in-memory dedupe cache for repeated payloads.
The request auth decorator is present in src/core/auth.py. It expects:
X-Timestamp: current Unix timestamp
X-Signature: HMAC_SHA256(OCR_SECRET_KEY, "{timestamp}.{path}")
Authentication is currently bypassed in code while OCR behavior is being developed. Re-enable it before exposing the API publicly.
For a new document type:
- Add a processor under
src/document_ocr/<document_type>/processor.py. - Keep the processor response shape consistent:
success,message,document_type,data, and optional diagnostics. - Add the route in
src/api/routes.py. - Add focused tests for missing files, invalid inputs, and a known-good sample.
For country-specific logic:
- Create a country package under
src/countries/<country>/, for examplesrc/countries/nigeria/. - Put country-specific aliases, supported document types, and validation helpers in that package.
- Register the country's
CountryProfileinsrc/countries/registry.py. - Keep shared OCR/parsing in the document processor.
- Return country codes and validation details explicitly in the response.
Current country-specific support:
NGA/ Nigeria- Passport MRZ country-code alias correction, such as
N6AorNG4toNGA. - Nigerian NIN card/slip metadata and parser support.
- Voter card parser support.
- Driver's license parser support.
- Additional local ID metadata: BVN and Tax Identification Number.
- Basic NIN format validation for exactly 11 digits.
- Passport MRZ country-code alias correction, such as
GHA/ Ghana- Passport MRZ country-code alias correction, such as
6HAtoGHA. - Voter ID parser support.
- Driver's license parser support.
- Starter local ID metadata: Ghana Card, Tax Identification Number, and SSNIT number.
- Passport MRZ country-code alias correction, such as
Processor naming rule:
- Use one canonical folder for the shared document family, such as
document_ocr/voter_id. - Put local country names in
src/countries/<country>/rules.py. - Put country-specific parsing differences in
src/countries/<country>/<document>.py.
Example:
src/
document_ocr/
voter_id/
processor.py
countries/
nigeria/
voter_id.py # Parses Nigeria Voter Card
rules.py # Exposes local code VOTER_CARD
ghana/
voter_id.py # Parses Ghana Voter ID
rules.py # Exposes local code VOTER_ID
Example response fragment for country-aware endpoints:
{
"country": {
"country_code": "NGA",
"country_name": "Nigeria",
"supported": true,
"checks": {
"document_type_supported": true,
"nin_format_valid": true
}
}
}When adding another country, keep the shape similar to src/countries/ghana/rules.py:
from src.countries.profile import CountryProfile
COUNTRY_PROFILE = CountryProfile(
code="ABC",
name="Example Country",
mrz_code_aliases={"ABC"},
supported_identity_documents={
"NATIONAL_ID": "National identity card",
"VOTER_ID": "Voter identity card",
},
)python -m pytest tests -vFor local development, install the test dependencies with:
pip install -r requirements-dev.txtRun the complete local quality gate with:
ruff format --check app.py src/api/routes.py src/core/document_source.py src/document_ocr/business_document src/document_ocr/text_extraction.py tests/test_business_document_*.py tests/test_document_source.py tests/test_url_input_api.py
ruff check app.py src/api/routes.py src/core/document_source.py src/document_ocr/business_document src/document_ocr/text_extraction.py tests/test_business_document_*.py tests/test_document_source.py tests/test_url_input_api.py
mypy
python -m pytest tests -vTests cover route behavior, parser rules, country profiles, page-aware text extraction, and legacy endpoint regressions using sanitized synthetic fixtures. OCR accuracy against real-world layouts still requires a controlled, legally usable document corpus.
- The API does not persist document uploads or URL downloads by default. Larger URL responses spill from memory to an unnamed temporary file and are closed after processing.
- URL ingestion expands the service's outbound-network surface. Keep the public-address checks enabled, consider a strict
DOCUMENT_URL_ALLOWED_HOSTSallowlist, restrict egress at the infrastructure layer, and apply rate/concurrency limits. - Business-document responses intentionally contain raw OCR text and evidence; treat them as sensitive and do not log them.
- Use a strong
OCR_SECRET_KEYbefore production deployment. - Re-enable HMAC verification before public exposure.
- Put rate limiting and upload-size limits at the reverse proxy or gateway layer.
- Webhook logs redact common sensitive headers.
MIT