Immigration paperwork looks like a legal problem until you actually map it out. Then it looks like a state machine with external dependencies, async validation steps, and no retry policy for human error.
I got interested in this after reading a breakdown of certified translation requirements for CPLP residency applications in Portugal. The source article covers the legal side well: which documents need certification, why Portugal doesn't have a 'sworn translator' system, and why Angola and Mozambique require a different legalisation path than Brazil or Cape Verde.
What it doesn't cover, and what caught my attention as a developer, is that this whole process maps cleanly onto a dependency graph. If you've ever built a workflow engine, an onboarding pipeline, or anything with conditional approval steps, you'll recognize the shape immediately.
This article is about modeling that process, not about giving legal advice. If you're actually going through this, talk to a lawyer. If you're building tooling to help people track it, read on.
Why this is a graph problem, not a checklist
Most people treat document requirements as a flat checklist: birth certificate, criminal record, proof of funds, done. But the actual dependency structure has branches based on country of origin.
Document
-> requires_translation? (based on source language)
-> requires_apostille? (based on Hague Convention status of issuing country)
-> requires_consular_legalisation? (if NOT in Hague Convention)
-> requires_certification? (lawyer/notary sign-off in destination country)
The branch point is country-specific. Brazil and Cape Verde are Hague Apostille signatories, so documents from there skip consular legalisation and go straight to apostille, then certified translation. Angola and Mozambique are not signatories, so documents need full consular legalisation before translation can even start.
If you're building any kind of tracker or checklist generator for this, the country of origin needs to be a first-class input that changes the entire downstream path, not a cosmetic label.
Modeling it as a small state machine
Here's a simplified version in Python, just to make the branching concrete:
from dataclasses import dataclass
from enum import Enum
class ApostilleStatus(Enum):
SIGNATORY = "signatory"
NON_SIGNATORY = "non_signatory"
HAGUE_STATUS = {
"BR": ApostilleStatus.SIGNATORY,
"CV": ApostilleStatus.SIGNATORY,
"ST": ApostilleStatus.SIGNATORY,
"AO": ApostilleStatus.NON_SIGNATORY,
"MZ": ApostilleStatus.NON_SIGNATORY,
}
@dataclass
class DocumentRequirement:
needs_translation: bool
needs_apostille: bool
needs_consular_legalisation: bool
needs_certification: bool
def resolve_requirements(country_code: str, source_language: str) -> DocumentRequirement:
status = HAGUE_STATUS.get(country_code, ApostilleStatus.NON_SIGNATORY)
needs_translation = source_language.lower() != "pt"
return DocumentRequirement(
needs_translation=needs_translation,
needs_apostille=status == ApostilleStatus.SIGNATORY,
needs_consular_legalisation=status == ApostilleStatus.NON_SIGNATORY,
needs_certification=needs_translation,
)
This is intentionally minimal, but it already surfaces the core insight from the source article: the apostille/legalisation branch and the translation/certification branch are independent, but legalisation has to resolve before translation starts. Get that order wrong (translate first, legalise after) and you restart from zero, which is exactly the failure mode called out in the original piece.
Why the Hague status table shouldn't be hardcoded
The dictionary above is a trap if you ship it as-is. The HCCH status table is reviewed periodically, and country status can change. If you're building anything real here:
- Treat the Hague signatory list as external, versioned data, not a constant in your codebase
- Add a "last verified" timestamp to whatever data source you use
- Flag stale data in your UI rather than silently trusting a six-month-old list
A simple approach is pulling the table into a JSON file on a schedule:
import httpx
import json
from datetime import datetime, timezone
def refresh_hague_status(output_path: str):
# pseudo-fetch, actual scraping/parsing depends on page structure
response = httpx.get("https://www.hcch.net/en/instruments/conventions/status-table/?cid=41")
data = parse_status_table(response.text) # your parser here
payload = {
"fetched_at": datetime.now(timezone.utc).isoformat(),
"countries": data,
}
with open(output_path, "w") as f:
json.dump(payload, f, indent=2)
Run this on a cron job, not on every request. Legal status data doesn't need to be real-time, but it does need to be auditable.
The consistency check that actually matters: name matching
The source article flags inconsistent names and dates across documents as a top cause of delays. This is worth automating because it's a solvable string-matching problem, not a legal judgment call.
A basic transliteration consistency check:
import unicodedata
def normalize_name(name: str) -> str:
nfkd = unicodedata.normalize('NFKD', name)
ascii_name = nfkd.encode('ASCII', 'ignore').decode('ASCII')
return ascii_name.lower().strip()
def names_likely_match(name_a: str, name_b: str) -> bool:
return normalize_name(name_a) == normalize_name(name_b)
This won't catch every edge case (hyphenation differences, dropped middle names, transliteration from non-Latin scripts), but it catches the common one: the same name typed slightly differently across a birth certificate and a criminal record certificate because they were issued by different offices years apart. Running this check before submission, rather than after a rejection, is the entire value proposition.
Building a status dashboard instead of a spreadsheet
If you're an immigration law firm, a relocation service, or just someone managing this for a family member, resist the urge to track this in a spreadsheet. A minimal web app with per-document status (pending, apostilled, legalised, translated, certified, submitted) gives you:
- A clear view of which documents are blocking submission
- Audit trail of when each step completed
- The ability to flag documents stuck in the consular legalisation queue, which is often the longest and least predictable step
Even a single-table SQLite setup with a status enum per document beats a shared spreadsheet where someone overwrites a cell and loses the history.
Takeaway
The legal content in the original M21Global article is the part you can't automate: knowing that Portugal uses a notary/lawyer certification model instead of a sworn translator registry, or that Angola and Mozambique require consular legalisation. That's domain knowledge, and you should get it from people who do this professionally.
But the process around that knowledge, tracking status, catching inconsistencies, keeping reference data fresh, is exactly the kind of workflow problem developers solve constantly in other domains. If you're building internal tools for a relocation service or law firm, or just trying to survive your own paperwork, treat it like a dependency graph with external data sources, not a static checklist.
Top comments (0)