Age checks have moved from a checkbox to an engineering task. In the UK, the Online Safety Act requires some services to use age checks that are "highly effective". In the EU, the European Commission has published a blueprint for proving you are over 18 without handing over anything else. Both name identity documents as one way in.

This post is for developers who have been asked to "add age verification" and want to know where an ID document scan fits. It covers which methods the UK and EU authorities list, what a scan of a passport or ID card does and does not prove, how to turn a date of birth into an over/under answer, and what to keep afterwards. Every quotation below comes from the authority's own page, linked where it appears.

This is the blog of doc.cheap, a Passport and ID OCR API. Read the product part with that in mind.

The UK: "highly effective" is the bar

The duty sits in section 12 of the Online Safety Act 2023. For the most harmful kinds of content, the provider must "use age verification or age estimation (or both) to prevent children of any age from encountering primary priority content that is harmful to children". And the check must be "of such a kind, and used in such a way, that it is highly effective at correctly determining whether or not a particular user is a child."

Section 230 defines the two terms. Age verification "means any measure designed to verify the exact age of users" of a service in scope. Age estimation "means any measure designed to estimate the age or age-range of users". It also closes the easiest shortcut: "A measure which requires a user to self-declare their age (without more) is not to be regarded as age verification or age estimation."

Ofcom, the UK communications watchdog, explains the bar in its guidance on highly effective age assurance for Part 3 services (the updated version is dated 24 April 2025). Section 3 gives "a non-exhaustive list" of methods it considers capable of being highly effective:

Method on Ofcom's list Where an ID document is involved
Open banking No
Photo-identification (photo-ID) matching Yes, the document is the evidence
Facial age estimation No
Mobile-network operator age checks No
Credit card checks No
Email-based age estimation No
Digital identity services Often, at the point the identity is first verified

Ofcom describes photo-ID matching like this: it "works by capturing relevant information from an uploaded photo-ID document and comparing it to an image of the user at the point of ID upload to verify that they are the same person."

The same section lists what does not qualify, including "asking a user to input their date of birth without further evidence to confirm this information". A typed date of birth is self-declaration. A date of birth read from a document is evidence, and the rest of Ofcom's guidance is about how good that evidence has to be.

The EU: a blueprint built on proof of age

On the EU side, the Commission released the first version of an age-verification blueprint on 14 July 2025. In its words, the blueprint "provides a method to enable users to prove they are over 18 when accessing restricted adult content, such as online pornography, without revealing any other personal information."

The design separates the check from the use. A user's "age will be verified by the issuer using detailed personal data, like the date of birth. However, online services will only receive a proof that the user is over 18, without any other personal details."

Identity documents come in at the first step. The Commission's fact page on the blueprint lists the sources of a proof of age as "national eIDs, passports and ID cards, pre-installed apps with age info, such as banking apps, or offline third-party activation, such as post office." It adds that "In October 2025, a second version of the blueprint was released with a passport and identity card method to generate a proof of age."

So in both places a document is an accepted input. In neither is a document photo on its own the whole check.

What a scan proves, and what it does not

A document scan answers one question well: what does this document say? It reads the date of birth, the expiry date, the document type and the machine-readable zone, and it can tell you whether the zone's check digits add up. That is the "capturing relevant information" part of Ofcom's description.

It does not answer the other questions an age check has to cover:

  • Is the person the holder? Ofcom's robustness section names the obvious route: "a child user uploading an image of an ID that does not belong to them." Its answer is that "Requiring a photo of the user at the point of ID upload when photo-ID matching helps to verify that the photo ID belongs to that user."
  • Is the person present? "Liveness detection provides further confidence that a child user has not uploaded a photo of an adult by ensuring that the user undergoing the age assurance process is present at the time the check is carried out."
  • Is the document genuine? "A robust photo-ID check should not be capable of being easily circumvented, and as such, a photo-ID method should be able to detect falsified documentation or manipulation that a child could create or obtain."

None of these is OCR. Reading a document and verifying it are separate jobs, done by separate tools, and an age check built on documents needs both. The reading step gives you trustworthy data about the document; the face match, the liveness check and the authenticity check tell you whether to trust the document and the person.

From a date of birth to over or under

Once the date of birth is read, the age arithmetic is small but easy to get wrong. Subtracting years is not enough: someone born on 20 November 2008 is 17 on 10 November 2026, not 18. Compare month and day too.

from datetime import date


def age_on(birth_iso: str, on: date) -> int:
    """Whole years completed on the day `on`, from a YYYY-MM-DD date of birth."""
    born = date.fromisoformat(birth_iso)
    years = on.year - born.year
    if (on.month, on.day) < (born.month, born.day):
        years -= 1  # the birthday has not come yet this year
    return years


def is_over(birth_iso: str, threshold: int, on: date) -> bool:
    return age_on(birth_iso, on) >= threshold


print(age_on("2008-11-20", date(2026, 11, 10)))     # 17
print(is_over("2008-11-20", 18, date(2026, 11, 20)))  # True

A few details matter in practice:

  • Use the date of the check, in one time zone. "Today" on the server and "today" for the user can differ near midnight. Pick one rule and log the date you used.
  • 29 February. With the comparison above, a person born on 29 February reaches a new age on 1 March in years without one. If your jurisdiction defines it differently, change that one branch.
  • A missing date is not an answer. If the scan returns no date of birth, the result is "could not check", never "over 18".
  • Expiry is a separate question. Whether an expired document is acceptable evidence is your policy, so read the expiry date and decide it explicitly.

Keep the answer, not the document

The Commission's design sends services "only a proof that the user is over 18". The same idea works in your own system: once the check is done, the result you need is a boolean and a date, not a passport image.

Ofcom's privacy section points the same way. It reminds providers that "All age assurance methods involve the processing of personal data and should follow a data protection by design approach", and lists data minimisation and storage limitation among the principles to follow.

A minimal record looks like this:

{
  "user_id": "u_3f9c",
  "check": "age_over_18",
  "result": true,
  "checked_on": "2026-11-10",
  "method": "photo_id_matching"
}

No image, no document number, no date of birth. If a law requires you to keep more, keep exactly that, in one store with its own deletion date.

Where doc.cheap fits

doc.cheap reads passports, ID cards and driving licences and returns what is printed as JSON. For an age check, the useful fields are holder.birth_date, an ISO YYYY-MM-DD date or null, plus document.expiry_date, document.is_expired and mrz.status, so the date you feed into age_on comes from a read you can sanity-check.

It is a reading service, not a verification one. Under the recognition-only scenario the response's authenticity.overall is not_checked, and the face match and liveness steps stay with tools built for them. It also keeps nothing you do not ask for: retain_hours: 0 on a live key writes no row at all, which the history retention guide covers. The image itself is never stored.

To try the reading step before you design the rest, start with the free passport OCR API. If you are choosing a provider, the passport OCR API comparison puts the published prices side by side.

This is an engineering summary, not legal advice.

A question for you: which part of your age check took the longest to get right – the reading, the face match or the record you keep? Tell us in the comments below.