AINode.jsAutomation
ID Card & Document OCR with AI Validation
TT
TopicTrick Team
ID Card & Document OCR with AI Validation
Manual ID verification is slow and error-prone. This tool photographs or uploads identity documents — driver's licenses, passports, national ID cards — and extracts all fields automatically using GPT-4o Vision, then validates the data for completeness and consistency.
This is Tool 21 of the Build 50 AI Automation Tools course.
What You'll Build
POST /verify— upload an ID document image, receive extracted and validated data- Supports driver's licenses, passports, and national ID cards
- Validates dates, expiry status, and field format consistency
Setup
bash
mkdir id-ocr && cd id-ocr
npm init -y
npm install express multer openai dotenvbash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000ID Extraction Service
js
// src/services/idService.js
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function extractIdData(buffer, mimetype, documentType = 'auto') {
const dataUrl = `data:${mimetype};base64,${buffer.toString('base64')}`;
const docTypeHint = documentType === 'auto'
? 'Automatically detect the document type.'
: `This is a ${documentType}.`;
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are a KYC document processing system. ${docTypeHint}
Extract all text and data from the identity document image.
Return ONLY a JSON object — no markdown:
{
"documentType": "drivers_license | passport | national_id | residence_permit | other",
"issuingCountry": "ISO 3166-1 alpha-2 country code (e.g. US, GB, DE)",
"issuingState": "state/province if applicable (e.g. CA for California) or null",
"firstName": "string",
"middleName": "string or null",
"lastName": "string",
"fullName": "string as it appears on document",
"dateOfBirth": "YYYY-MM-DD format",
"gender": "M | F | X | null",
"nationality": "string or null",
"documentNumber": "string",
"expiryDate": "YYYY-MM-DD format",
"issueDate": "YYYY-MM-DD format or null",
"address": "string or null (for licenses with address)",
"mrzLine1": "string or null (passport machine readable zone line 1)",
"mrzLine2": "string or null (passport machine readable zone line 2)",
"restrictions": "string or null (license restrictions)",
"documentClass": "string or null (license class/category)",
"confidence": 0.0-1.0
}`,
},
{
role: 'user',
content: [
{ type: 'text', text: 'Extract all data from this identity document:' },
{ type: 'image_url', image_url: { url: dataUrl, detail: 'high' } },
],
},
],
temperature: 0,
response_format: { type: 'json_object' },
});
return JSON.parse(response.choices[0].message.content);
}
export function validateIdData(data) {
const errors = [];
const warnings = [];
const today = new Date();
// Check expiry
if (data.expiryDate) {
const expiry = new Date(data.expiryDate);
if (isNaN(expiry.getTime())) {
errors.push('Invalid expiry date format');
} else if (expiry < today) {
errors.push(`Document expired on ${data.expiryDate}`);
} else {
const daysUntilExpiry = Math.floor((expiry - today) / (1000 * 60 * 60 * 24));
if (daysUntilExpiry < 90) {
warnings.push(`Document expires in ${daysUntilExpiry} days`);
}
}
} else {
errors.push('Expiry date could not be extracted');
}
// Check date of birth
if (data.dateOfBirth) {
const dob = new Date(data.dateOfBirth);
if (isNaN(dob.getTime())) {
errors.push('Invalid date of birth format');
} else {
const age = Math.floor((today - dob) / (1000 * 60 * 60 * 24 * 365.25));
if (age < 0 || age > 120) {
errors.push(`Implausible date of birth: age would be ${age}`);
}
}
} else {
errors.push('Date of birth could not be extracted');
}
// Required fields
const required = ['firstName', 'lastName', 'documentNumber', 'expiryDate', 'dateOfBirth'];
required.forEach(field => {
if (!data[field]) errors.push(`Missing required field: ${field}`);
});
// MRZ validation for passports
if (data.documentType === 'passport' && data.mrzLine1 && data.mrzLine2) {
if (data.mrzLine1.length !== 44 || data.mrzLine2.length !== 44) {
warnings.push('MRZ line length does not match standard passport format');
}
}
return {
isValid: errors.length === 0,
errors,
warnings,
isExpired: data.expiryDate ? new Date(data.expiryDate) < today : null,
};
}Server
js
// src/server.js
import 'dotenv/config';
import express from 'express';
import multer from 'multer';
import { extractIdData, validateIdData } from './services/idService.js';
const app = express();
app.use(express.json());
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 },
fileFilter: (_req, file, cb) =>
['image/jpeg', 'image/png', 'image/webp'].includes(file.mimetype)
? cb(null, true) : cb(new Error('Image files only')),
});
app.post('/verify', upload.single('document'), async (req, res, next) => {
try {
if (!req.file) return res.status(400).json({ error: 'No document image uploaded' });
const { documentType } = req.body;
const extracted = await extractIdData(req.file.buffer, req.file.mimetype, documentType);
const validation = validateIdData(extracted);
res.json({
success: true,
extracted,
validation,
status: validation.isValid ? 'PASS' : 'FAIL',
});
} catch (err) { next(err); }
});
app.use((err, _req, res, _next) => res.status(500).json({ error: err.message }));
app.listen(process.env.PORT ?? 3000, () => console.log('ID OCR running'));Testing
bash
curl -X POST http://localhost:3000/verify \
-F "document=@drivers-license.jpg" \
-F "documentType=drivers_license"Sample response:
json
{
"extracted": {
"documentType": "drivers_license",
"issuingCountry": "US",
"issuingState": "CA",
"firstName": "John",
"lastName": "Smith",
"dateOfBirth": "1990-03-15",
"documentNumber": "D1234567",
"expiryDate": "2027-03-15",
"address": "123 Main St, San Francisco, CA 94105",
"confidence": 0.94
},
"validation": {
"isValid": true,
"errors": [],
"warnings": [],
"isExpired": false
},
"status": "PASS"
}Build 50 AI Automation Tools — Tool 21 of 50
ID document OCR is live. Phase 4 complete. Continue to Phase 5 with Tool 22: AI Code Reviewer for Pull Requests.
Summary
- detail: 'high' is critical for ID documents — low-resolution processing misses small text fields
- Post-extraction validation catches errors the AI might make — always validate dates and required fields
- Confidence score enables human-in-the-loop review for low-confidence extractions
- MRZ validation adds an extra check layer for passport documents
- Never store raw document images — process in memory and discard immediately in production
Continue to Tool 22: AI Code Reviewer for Pull Requests →
Post Navigation (Previous/Next)
