ANPR API for Clean Air Zone Compliance

Jul 13, 2026 · 12 min read

Why Clean Air Zone Compliance Is Now an ANPR Problem

Clean air zones have become one of the most data-intensive compliance problems in UK transport. There are currently seven cities charging under a Clean Air Zone framework in England: Bath, Birmingham, Bradford, Bristol, Portsmouth, Sheffield, and Tyneside (Newcastle and Gateshead). London's ULEZ now covers all 32 boroughs following its August 2023 expansion to the entire Greater London area. Scotland's Low Emission Zones in Glasgow, Edinburgh, Aberdeen and Dundee have all been fully enforced since June 2024. The London Congestion Charge rose to £18 per day from 2 January 2026. A non-compliant vehicle subject to both ULEZ and the Congestion Charge faces £30.50 in daily exposure before a single Penalty Charge Notice is issued.

For fleet operators, car park management platforms, logistics dispatchers and mobility apps, this is no longer a policy problem to hand off to a compliance team. It is an integration problem that must be solved at the point of camera capture. The question is simple: given a plate read from a camera feed, can you determine within a few hundred milliseconds whether that vehicle will attract a charge, before the driver proceeds any further? This article walks through exactly how to build that check using two REST APIs chained together.

How ULEZ and CAZ Enforcement Actually Works

There are no barriers. ANPR cameras photograph every vehicle entering or moving within the zone. The plate string is resolved against a vehicle database, the emissions profile is checked, and if the vehicle is non-compliant and no payment has been recorded, the owner receives a Penalty Charge Notice by post. The entire process is automated. For ULEZ, drivers must pay the £12.50 daily charge by midnight on the third day after driving in the zone. Miss that window and a PCN of £160 is issued, reduced to £80 if paid within 14 days. Recipients have 28 days to pay or formally challenge the notice.

The enforcement architecture is the same across CAZ cities: ANPR cameras read plates, a back-end system resolves each plate to a vehicle record, and compliance is assessed against Euro emission standards. This means any application that can read a plate and look up the associated emission data can replicate the same compliance check that enforcement systems perform, in real time, before a charge is ever triggered.

The Compliance Data Model

To determine whether a UK-registered vehicle is subject to a ULEZ or CAZ charge, you need four fields. The fuel type tells you which Euro standard applies. The Euro standard, whether inferred or returned directly, tells you whether the vehicle passes. The tax class reveals exemptions. The date of first registration provides a fallback when the Euro standard is absent from the record.

In any of the current zones, the minimum standards for compliance are Euro 4 for petrol cars and vans (generally vehicles registered from January 2006) and Euro 6 for diesel cars and vans (generally vehicles registered from September 2015). Motorcycles must meet Euro 3, generally vehicles registered after July 2007 under ULEZ rules, but are exempt entirely from the London Congestion Charge. Electric and hydrogen vehicles produce no tailpipe emissions and are therefore compliant with all current ULEZ and CAZ emission standards.

All four data points live in the DVLA Vehicle Enquiry Service (VES) response. The VES API is a RESTful service that accepts a vehicle registration number and returns vehicle details in JSON format, including fuelType, yearOfManufacture, monthOfFirstRegistration, taxStatus, euroStatus and co2Emissions. When euroStatus is present it gives you the answer directly. When it is absent, you fall back to registration year combined with fuel type.

Step 1: Reading the Plate with NPR API

The first link in the chain is turning a camera image into a structured plate string. Send a POST request to https://nprapi.com/api/v1/recognise with your key in the X-API-Key header and the image as a multipart/form-data file field named image. Add vehicle=true as a form field to retrieve make, model and colour alongside the plate, which is useful for cross-checking the DVLA response.

curl -X POST https://nprapi.com/api/v1/recognise \
  -H "X-API-Key: your-api-key-here" \
  -F "image=@entry_lane.jpg" \
  -F "vehicle=true"

The response is clean JSON:

{
  "success": true,
  "registration": "LK21ABC",
  "confidence": 94,
  "make": "VOLKSWAGEN",
  "model": "GOLF",
  "colour": "GREY",
  "credits_used": 1
}

The confidence field is an integer from 0 to 100. Treat any read below a threshold you set during testing, typically in the range of 80 to 85, as requiring manual review rather than automated enforcement action. The low-confidence section below covers how to handle these gracefully.

Step 2: Chaining to the DVLA Vehicle Enquiry Service

Once you have the registration string, pass it immediately to the DVLA VES. The endpoint is https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles. Authentication uses an x-api-key header. The request body is JSON with a single registrationNumber field.

POST https://driver-vehicle-licensing.api.gov.uk/vehicle-enquiry/v1/vehicles
Content-Type: application/json
x-api-key: your-dvla-api-key

{
  "registrationNumber": "LK21ABC"
}

A successful response looks like this:

{
  "registrationNumber": "LK21ABC",
  "fuelType": "DIESEL",
  "yearOfManufacture": 2021,
  "monthOfFirstRegistration": "2021-03",
  "taxStatus": "Taxed",
  "colour": "GREY",
  "make": "VOLKSWAGEN",
  "euroStatus": "EURO 6 DG",
  "co2Emissions": 114
}

The fields you need for compliance are fuelType, yearOfManufacture, monthOfFirstRegistration, taxStatus and, when present, euroStatus. Register for a DVLA VES API key through the DVLA developer portal at developer-portal.driver-vehicle-licensing.api.gov.uk. Note that the DVLA periodically pauses new registrations while making system upgrades, so check the portal for current availability.

Step 3: Writing the Euro Standard Inference Logic

When euroStatus is present, use it directly. When it is absent, which is common for older vehicles whose records pre-date the field, derive compliance from fuelType and monthOfFirstRegistration. The logic maps cleanly to the published thresholds for all live zones.

Here is a worked Python implementation:

from datetime import date

PETROL_EURO4_CUTOFF = date(2006, 1, 1)   # Euro 4 petrol: generally from Jan 2006
DIESEL_EURO6_CUTOFF = date(2015, 9, 1)   # Euro 6 diesel: generally from Sep 2015
ELECTRIC_FUELS = {"ELECTRICITY", "HYDROGEN"}

def is_ulez_compliant(dvla: dict) -> dict:
    """
    Returns a dict with 'compliant' (bool), 'reason' (str)
    and 'euro_standard' (str or None).
    """
    fuel = dvla.get("fuelType", "").upper()
    euro_status = dvla.get("euroStatus", "")
    tax_class = dvla.get("taxStatus", "").upper()
    first_reg_str = dvla.get("monthOfFirstRegistration", "")

    # Electric and hydrogen are always compliant
    if fuel in ELECTRIC_FUELS:
        return {"compliant": True, "reason": "Zero-emission fuel type", "euro_standard": "N/A"}

    # Disabled tax class is exempt
    if "DISABLED" in tax_class:
        return {"compliant": True, "reason": "Disabled tax class exemption", "euro_standard": None}

    # Use euroStatus directly if available
    if euro_status:
        euro_upper = euro_status.upper()
        if fuel == "PETROL":
            compliant = any(s in euro_upper for s in ["EURO 4", "EURO 5", "EURO 6", "EURO 7"])
        elif fuel == "DIESEL":
            compliant = any(s in euro_upper for s in ["EURO 6", "EURO 7"])
        else:
            compliant = False
        return {"compliant": compliant, "reason": f"euroStatus field: {euro_status}", "euro_standard": euro_status}

    # Fall back to registration date inference
    if not first_reg_str:
        return {"compliant": None, "reason": "Insufficient data for inference", "euro_standard": None}

    year, month = int(first_reg_str[:4]), int(first_reg_str[5:7])
    reg_date = date(year, month, 1)

    if fuel == "PETROL":
        compliant = reg_date >= PETROL_EURO4_CUTOFF
        standard = "Euro 4+" if compliant else "Pre-Euro 4"
    elif fuel == "DIESEL":
        compliant = reg_date >= DIESEL_EURO6_CUTOFF
        standard = "Euro 6+" if compliant else "Pre-Euro 6"
    else:
        compliant = None
        standard = None

    return {"compliant": compliant, "reason": "Date-based inference (euroStatus absent)", "euro_standard": standard}

The same logic in Node.js:

const PETROL_EURO4_CUTOFF = new Date('2006-01-01');
const DIESEL_EURO6_CUTOFF = new Date('2015-09-01');
const ELECTRIC_FUELS = new Set(['ELECTRICITY', 'HYDROGEN']);

function isUlezCompliant(dvla) {
  const fuel = (dvla.fuelType || '').toUpperCase();
  const euroStatus = dvla.euroStatus || '';
  const taxStatus = (dvla.taxStatus || '').toUpperCase();
  const firstReg = dvla.monthOfFirstRegistration || '';

  if (ELECTRIC_FUELS.has(fuel)) {
    return { compliant: true, reason: 'Zero-emission fuel type', euroStandard: 'N/A' };
  }

  if (taxStatus.includes('DISABLED')) {
    return { compliant: true, reason: 'Disabled tax class exemption', euroStandard: null };
  }

  if (euroStatus) {
    const e = euroStatus.toUpperCase();
    let compliant = false;
    if (fuel === 'PETROL') compliant = /EURO [4567]/.test(e);
    else if (fuel === 'DIESEL') compliant = /EURO [67]/.test(e);
    return { compliant, reason: `euroStatus field: ${euroStatus}`, euroStandard: euroStatus };
  }

  if (!firstReg) return { compliant: null, reason: 'Insufficient data', euroStandard: null };

  const regDate = new Date(firstReg + '-01');
  let compliant, standard;

  if (fuel === 'PETROL') {
    compliant = regDate >= PETROL_EURO4_CUTOFF;
    standard = compliant ? 'Euro 4+' : 'Pre-Euro 4';
  } else if (fuel === 'DIESEL') {
    compliant = regDate >= DIESEL_EURO6_CUTOFF;
    standard = compliant ? 'Euro 6+' : 'Pre-Euro 6';
  } else {
    compliant = null; standard = null;
  }

  return { compliant, reason: 'Date-based inference (euroStatus absent)', euroStandard: standard };
}

Edge Cases Developers Must Handle

Foreign-registered plates

The DVLA VES only holds records for UK-registered vehicles. Foreign plates that are unrecognised in TfL's database are automatically treated as non-compliant until manually reviewed. When the VES returns a 404 for a given plate, your application must flag the record for human review rather than asserting compliance or non-compliance. NPR API returns a country code in multiple-plate mode, which helps you identify non-UK plates early. Enable the multiple=true flag and inspect the ISO 3166-1 alpha-2 country field on each plate object in the plates array.

Disabled tax class

Vehicles registered under the disabled or disabled passenger vehicle tax class have a ULEZ exemption grace period running to October 2027. The taxStatus field in the VES response will reflect this. Your compliance function should check for DISABLED in the tax status string before evaluating any emission standard, as shown in the code above.

Hire and lease vehicles

The ANPR system and the PCN are directed at the registered keeper, not the driver. For hire fleets, this means the rental company receives the charge notification. If your platform manages a hire fleet, you will need to track the driver assignment at the time of the plate read, not just the keeper record from the DVLA, in order to pass costs through correctly. The VES does not return keeper identity data, by design, so this mapping must come from your own fleet management layer.

Electric and hydrogen vehicles

When fuelType is ELECTRICITY or HYDROGEN, the vehicle is compliant with all current ULEZ and CAZ emission standards. However, since 25 December 2025, electric vehicles are no longer fully exempt from the London Congestion Charge. From 2 January 2026, when the charge rose to £18 per day, electric vehicles registered for TfL Auto Pay qualify for a 25% discount, paying £13.50 per day. Your compliance logic must treat ULEZ and Congestion Charge compliance as two separate evaluations.

Motorcycles

Motorcycles and mopeds must meet Euro 3 standards for ULEZ compliance, but are exempt from the London Congestion Charge entirely. Scotland's Low Emission Zones, including Glasgow, Edinburgh, Aberdeen and Dundee, also exempt motorcycles and mopeds. Your zone-aware compliance function must branch on vehicle class as well as fuel type.

Scottish LEZ distinctions

Unlike English CAZs and the London ULEZ, Scottish Low Emission Zones do not operate on a pay-to-drive model. Non-compliant vehicles are banned from entering the zone entirely, and drivers who do so receive a penalty notice rather than a daily charge. Scotland also applies a 30-year historic vehicle exemption, meaning vehicles first registered 30 or more years ago are automatically exempt provided they remain substantially unmodified. Build this distinction into any zone-aware routing or alerting logic.

Handling High-Throughput Entry Lanes

A busy car park entry lane or logistics depot gate may see a vehicle every few seconds. Blocking the control system on two sequential REST calls, one to NPR API and one to DVLA VES, adds latency that can back up a queue. The webhook pattern solves this cleanly.

Configure NPR API to POST plate-read results to your endpoint asynchronously. When a camera detects a plate, NPR API pushes the structured JSON to your webhook URL. Your handler receives the plate string, fires the DVLA VES lookup in the background, evaluates compliance, and updates your database record. The barrier or gate decision is made from that cached record on the next polling cycle, typically within one to two seconds of the original plate read, without the entry lane process waiting on either API call.

For batch camera feeds, use the NPR API batch endpoint at https://nprapi.com/api/v1/batch, submitting multiple images as images[] fields, and poll the job status via GET https://nprapi.com/api/v1/batch/{uuid}. This is well-suited to processing overnight camera footage from car parks where compliance checks feed a morning report rather than a real-time gate decision.

When the Confidence Score Is Low

The NPR API confidence field is an integer from 0 to 100. A score in the high eighties or above is generally reliable for automated decision-making, but the right threshold depends on your camera hardware, mounting angle and lighting conditions. Calibrate against your own test footage during development.

When a read falls below your threshold, do not assert compliance or non-compliance. Instead, route the record to a manual review queue, log the image path, the raw plate string, the confidence score and the timestamp, and surface a flag in your operator dashboard. For mobility apps and fleet portals, this maps to an uncertain state in the UI: a warning indicator rather than a green pass or a red fail. An incorrect non-compliant flag on a compliant vehicle could trigger an unfair charge, while an incorrect compliant flag on a non-compliant vehicle defeats the purpose of the check entirely.

GDPR and Data Retention

When you call the NPR API recognise endpoint, the image is processed and the plate string is returned. The API does not retain the image after the call completes. This zero-retention model matters for GDPR compliance: you are not accumulating a database of vehicle movements and associated images on a third-party server. The plate string you receive is data you control, and your own retention and lawful-basis obligations govern how long you store it and for what purpose.

If you are building a fleet compliance tool rather than an enforcement system, a short retention window, sufficient only to confirm the compliance check was performed and log the outcome, is appropriate. Document your lawful basis, whether legitimate interest or contract performance, and review it against ICO guidance for ANPR operators before going live.

Putting It All Together

Clean air zone compliance is, at its core, a data pipeline: image in, plate string out, vehicle record fetched, Euro standard inferred, compliance flag set. NPR API handles the first two steps with a single POST call returning structured JSON. The DVLA Vehicle Enquiry Service handles the third. The inference logic in the examples above handles the fourth. The flag is yours to act on, whether that means routing a logistics vehicle away from a zone, alerting a fleet manager, pricing a car park booking dynamically, or logging a compliance audit trail.

The edge cases covered above are predictable: foreign plates, disabled exemptions, hire keeper mismatches, Scottish LEZ bans, and the now-separate ULEZ and Congestion Charge evaluations for electric vehicles. Build them into your compliance function from the start rather than discovering them in production through incorrect PCN attributions or missed exemptions.

To get started, sign up for the NPR API free tier at nprapi.com/docs and make a test call with a sample plate image to verify the JSON response shape matches what the code above expects. Then register for a DVLA VES API key through the DVLA developer portal to access the vehicle record data. Both APIs are fully documented and both support test environments, so you can build and validate the full compliance chain before connecting a single live camera.

Ready to integrate number plate recognition?

Get Started Free