AINode.jsAutomation

ID Card & Document OCR with AI Validation

TT
TopicTrick Team
ID Card & Document OCR with AI Validation

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 dotenv
bash
# .env
OPENAI_API_KEY=sk-your-key-here
PORT=3000

ID 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 →