DEV Community

Cover image for Building a Document Requirements Checklist API for International Legal Workflows
Diogo Heleno
Diogo Heleno

Posted on Originally published at m21global.com

Building a Document Requirements Checklist API for International Legal Workflows

The problem: requirements live in people's heads, not in systems

If you've ever worked on an internal tool for legal ops, HR, or compliance teams, you've probably run into this pattern: someone orders a certified translation, a notarization, or an apostille, and weeks later it bounces back because a receiving authority had a formatting requirement nobody captured anywhere.

There's a good write-up on the human side of this problem in this article on certified translation rejections. The short version: certification standards vary wildly by country and even by institution, and the fix has to happen before the document is produced, not after rejection.

That's a workflow problem, and workflow problems are things we can actually model in software. This post walks through how to think about building a lightweight requirements-checklist system for document localization and certification pipelines, something you could bolt onto an internal tool, a client portal, or a document management system.

Why this isn't just a static form

The naive approach is a dropdown: "select destination country" then show a static list of requirements. That breaks almost immediately because requirements aren't purely country-level. They depend on:

  • Destination country (Portugal vs Brazil vs Angola have fundamentally different certification models)
  • Receiving institution type (court, university, notary, HR department)
  • Document type (articles of association vs birth certificate vs contract)
  • Hague Apostille Convention membership of the destination country
  • Direction of travel (document originating in Portugal going to Angola is a different legalisation chain than the reverse)

That's a multi-dimensional lookup, which means your data model needs to be a decision tree or rules engine, not a flat table.

A simple rules-based data model

Here's a minimal schema that captures the logic described in the source article:

{
  "country": "AO",
  "name": "Angola",
  "hagueMember": false,
  "swornTranslatorRegistry": false,
  "certificationModel": "notary-authenticated",
  "legalisationChain": {
    "inbound": [
      "origin_country_mfa_authentication",
      "angolan_consulate_legalisation"
    ],
    "outbound": [
      "mirex_legalisation"
    ]
  },
  "certificateValidityMonths": null
}
Enter fullscreen mode Exit fullscreen mode

And for a Hague member country:

{
  "country": "PT",
  "name": "Portugal",
  "hagueMember": true,
  "swornTranslatorRegistry": false,
  "certificationModel": "lawyer-or-notary-attested",
  "apostilleIssuer": "PGR",
  "certificateValidityMonths": 6
}
Enter fullscreen mode Exit fullscreen mode

The key fields that actually drive decisions are hagueMember (apostille vs consular legalisation), swornTranslatorRegistry (whether a local sworn translator is mandatory), and legalisationChain.inbound vs .outbound, since the direction matters a lot, as the source article points out with the MIREX example (it only legalises outbound Angolan documents, never inbound foreign ones).

A basic resolver function

Once you have the data modeled, the logic to generate a checklist is mostly conditionals:

function resolveRequirements({ originCountry, destCountry, direction, documentType }) {
  const dest = countryRules[destCountry];
  const origin = countryRules[originCountry];
  const checklist = [];

  if (dest.swornTranslatorRegistry) {
    checklist.push({
      type: "warning",
      message: `${dest.name} maintains a sworn translator registry. Confirm whether a translation certified in ${origin.name} is accepted, or if a locally sworn translator is required.`
    });
  }

  if (dest.hagueMember) {
    checklist.push({
      type: "action",
      message: `Apostille required (Hague Convention). Confirm whether it applies to the original, the translation, or both.`
    });
  } else {
    const chain = dest.legalisationChain[direction] || [];
    checklist.push({
      type: "action",
      message: `${dest.name} is not a Hague signatory. Legalisation chain: ${chain.join(" -> ")}`
    });
  }

  if (dest.certificateValidityMonths) {
    checklist.push({
      type: "info",
      message: `Certificate must be issued within ${dest.certificateValidityMonths} months of submission.`
    });
  }

  checklist.push({
    type: "action",
    message: "Get the above confirmed in writing (email) from the receiving authority before ordering translation."
  });

  return checklist;
}
Enter fullscreen mode Exit fullscreen mode

This is deliberately simple. The point isn't to build a legal advice engine, it's to force the right questions to surface before anyone places an order. That last line in the checklist matters as much as the logic: the system's job is to generate the questions to ask, not to pretend it has authoritative legal answers. Requirements change, authorities are inconsistent, and a resolver like this needs a disclaimer baked into the UI, not just the code comment.

Where this fits in a real pipeline

If you're building internal tooling for a company that regularly deals with cross-border documents (legal ops, immigration, HR for expats, procurement with international vendors), this kind of checklist generator slots in naturally as:

  • A pre-flight step in a document request form, before it ever reaches a vendor
  • A validation gate in a DMS workflow, blocking submission until checklist items are marked "confirmed with authority"
  • An audit trail generator, since the source article's advice to "get confirmation in writing" is exactly the kind of thing you want timestamped and stored, not left in someone's inbox

A practical addition: log each confirmed requirement with a timestamp and source (email, portal screenshot, phone log reference). When a document gets rejected six months later, you want to pull up exactly what was confirmed and when, not reconstruct it from memory.

function logConfirmation(checklistItemId, { confirmedBy, source, date, notes }) {
  return db.confirmations.insert({
    checklistItemId,
    confirmedBy,
    source,
    date,
    notes,
    createdAt: new Date().toISOString()
  });
}
Enter fullscreen mode Exit fullscreen mode

Maintaining the rules table

The hard part isn't the code, it's keeping the underlying rules accurate. Apostille Convention membership changes (several countries have joined in the past decade), and institutional requirements shift without much public notice.

Practical approach:

  • Treat the country rules table as data, not code, so non-engineers (legal ops, compliance) can update it
  • Version it and require sign-off on changes, since a wrong entry here causes real-world rejections
  • Add a lastVerified date field per country and flag entries older than, say, 12 months for review
  • Link each rule to a source document or email confirmation where possible

Takeaway

Rejected certified translations are rarely a quality problem, they're a missing-checklist problem. If your org handles this kind of document flow more than a handful of times a year, it's worth treating the "ask the receiving authority first" step as a structured, loggable part of the pipeline instead of tribal knowledge. The rules engine doesn't need to be sophisticated. It just needs to make the right questions impossible to skip.

Top comments (0)