# License Plate Reader — ANPR / LPR for Photos & Video (`adorable_partial/license-plate-reader`) Actor

Read license plates from photos and CCTV videos: plate text, confidence, plate box, vehicle make, model and color. Trained on real Brazilian road cameras (old and Mercosur plates). Video: one row per vehicle passing. No GPU, pay per image or minute.

- **URL**: https://apify.com/adorable_partial/license-plate-reader.md
- **Developed by:** [Leandro Zanatta](https://apify.com/adorable_partial) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 image processeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## License Plate Reader — ANPR / LPR API for Photos & Video

Read **license plates** from photos, camera snapshots and **CCTV videos**. For every plate you get the **plate text**, a **confidence score**, the **plate box**, and the **vehicle that carries it**: type, **make and model** and **color**. Videos return **one row per vehicle passing**, with the time it appeared and left, instead of hundreds of duplicate reads.

The models were trained on **real Brazilian road and street cameras** and read **old Brazilian plates (ABC-1234)** and **Mercosur plates (ABC1D23)**, plus Argentina, Paraguay and Uruguay layouts. Runs on CPU, no GPU or SDK to install.

![Plates read on three public photos: EUE4635 on a Fiat Uno, EPF5030 on a VW Voyage, KFG1623 on a car in the background, and KLV8465 on a plate close-up](https://api.apify.com/v2/key-value-stores/PpMKzEhiO1iwcSwLi/records/demo.png?signature=19GH7FDb7CdxF2QvHsmuQ)

*Real output of this Actor on public photos from Wikimedia Commons (CC BY 2.0 and public domain).*

### Why use it

- 🎯 **Accurate on real cameras**: **98% of plates read exactly right** (every character) on held-out crops from Brazilian road cameras, 95.6% on a larger mixed sample. Letter/digit mix-ups (O/0, I/1, B/8) are corrected using the plate layout.
- 🚗 **More than the plate**: vehicle type, make and model (over 200 cars, trucks and buses common in Brazil, plus motorcycles) and color, linked to each plate.
- 🎞️ **Video-aware**: the same plate seen in many frames becomes **one sighting** with first/last time, number of reads and the best crop.
- 🧹 **Clean results**: only reads that match a real plate format are returned (switch off to get everything).
- 💸 **Pay per use**: per image or per minute of video. No subscription, no per-camera licence. Failed files are free.
- 🧩 **Works anywhere**: API, schedules, webhooks, Make, Zapier, n8n and AI agents (MCP).

### Who uses it

| Sector | Typical use |
|---|---|
| **Parking lots, garages & condominiums** | Log entries and exits from gate cameras, check registered vehicles |
| **Logistics, yards & fleets** | Record trucks arriving at docks and yards, match them with orders |
| **Security companies & monitoring centres** | Search recorded footage for a plate after an incident |
| **Car washes, workshops & dealerships** | Identify returning customers' vehicles automatically |
| **Insurance & claims** | Extract plate, make, model and color from claim photos |
| **Toll, traffic & smart-city projects** | Count and classify vehicles, build origin-destination studies |
| **AI & data teams** | Label datasets, evaluate own models, pre-filter footage |

### What you get per plate

| Field | Meaning |
|---|---|
| `plate` | Plate text without dash, e.g. `EUE4635` or `RDY7G98` |
| `format` | `BR` (old Brazil), `BR-MERCOSUR`, `AR-MERCOSUR`, `AR`, `PY/UY` |
| `confidence` | Average character confidence (0–1) |
| `detectionConfidence` | How sure the detector is that the box is a plate |
| `box` | Plate position in pixels (`x1`, `y1`, `x2`, `y2`) |
| `rawText` | The read before layout correction (only when it changed) |
| `vehicle` | `type` (car, truck, van, bus, motorcycle…), `make`, `model`, `color`, their confidences and box |
| `plateImageUrl`, `vehicleImageUrl` | Crops, when *Save plate images* is on |

Make and model are only named when the detector is confident (about 81% right at that level); otherwise you still get the vehicle box and color.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| **Photos** (`imageUrls`) | array | — | JPEG, PNG, WebP, BMP, TIFF up to 50 MB. Plate close-ups and crops work too |
| **Videos** (`videoUrls`) | array | — | MP4, MKV, AVI, MOV, TS… up to 4 GB. Google Drive and Dropbox links work |
| **Frames read per second** (`framesPerSecond`) | string | `2` | 2 for gates, parking and city traffic; 4–5 for fast highway traffic |
| **Max minutes per video** (`maxDurationMinutes`) | integer | `120` | Only the first N minutes are read and billed |
| **Vehicle make, model and color** (`includeVehicle`) | boolean | `true` | Add the vehicle carrying each plate |
| **Save plate images** (`savePlateImages`) | boolean | `false` | Store crops of plates (and vehicles in videos) |
| **Only plates in a known format** (`onlyValidPlates`) | boolean | `true` | Drop partial and unknown-layout reads |
| **Minimum read confidence** (`minTextConfidence`) | string | `0.5` | Raise to 0.8 for fewer, surer reads |
| **Minimum plate detection confidence** (`minPlateConfidence`) | string | `0.25` | Detector threshold |

```json
{
  "videoUrls": ["https://example.com/nvr/gate-entrance-2026-10-12.mp4"],
  "framesPerSecond": "2",
  "savePlateImages": true
}
```

### Output

**Photos**: one row per image.

```json
{
  "sourceUrl": "https://example.com/gate/cam1-0042.jpg",
  "width": 1280, "height": 720,
  "platesFound": 1,
  "plateNumbers": ["RYN8A72"],
  "plates": [{
    "plate": "RYN8A72", "format": "BR-MERCOSUR", "confidence": 0.939, "detectionConfidence": 0.929,
    "box": { "x1": 483, "y1": 383, "x2": 565, "y2": 420 },
    "vehicle": { "type": "car", "make": "Hyundai", "model": "Hb20", "confidence": 0.983, "color": "white",
                 "colorConfidence": 0.979, "box": { "x1": 237, "y1": 151, "x2": 669, "y2": 485 } }
  }],
  "vehiclesDetected": 1
}
```

**Videos**: one row per plate sighting (a vehicle passing), plus a `SUMMARY-video001` record in the key-value store with all plate numbers, minutes read and billed.

```json
{
  "sourceUrl": "https://example.com/nvr/gate-entrance.mp4",
  "sighting": 3,
  "plate": "RDY7G98", "format": "BR-MERCOSUR", "confidence": 0.94,
  "firstSeenSeconds": 812.5, "lastSeenSeconds": 815.0, "bestFrameSeconds": 813.5, "reads": 6,
  "vehicle": { "type": "car", "make": "Hyundai", "model": "Hb20", "color": "white" },
  "plateImageUrl": "https://api.apify.com/v2/key-value-stores/.../records/video001-plate0003.jpg",
  "vehicleImageUrl": "https://api.apify.com/v2/key-value-stores/.../records/video001-vehicle0003.jpg"
}
```

Failed files return an `error` field and are not billed.

### Pricing

Pay per **image processed** or per **started minute of video read**, plus a tiny per-run start fee. Apify plan discounts apply automatically (see the **Pricing** tab).

| Example | Billed |
|---|---|
| 1 photo | 1 image |
| 1,000 claim photos | 1,000 images |
| 1 hour of a gate camera | 60 video minutes |

Use **Max minutes per video** or the run's *maximum cost* to cap spending; the Actor stops cleanly at the limit.

### Use it from code or an AI agent

**Python: plates entering a gate last night**

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("adorable_partial/license-plate-reader").call(run_input={
    "videoUrls": ["https://example.com/nvr/gate-night.mp4"],
    "savePlateImages": True,
})
for s in client.dataset(run["defaultDatasetId"]).iterate_items():
    if "plate" in s:
        v = s.get("vehicle") or {}
        print(f'{s["firstSeenSeconds"]:>8.1f}s  {s["plate"]}  {v.get("make")} {v.get("model")} {v.get("color")}')
```

**JavaScript: read plates in photos**

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('adorable_partial/license-plate-reader').call({
    imageUrls: ['https://example.com/parking/cam1-snapshot.jpg'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items[0].plateNumbers);
```

**HTTP (synchronous, one call)**

```bash
curl -X POST "https://api.apify.com/v2/acts/adorable_partial~license-plate-reader/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"imageUrls": ["https://example.com/snapshot.jpg"]}'
```

**No-code and agents:** send camera snapshots from **Make**, **Zapier** or **n8n**, trigger runs from **webhooks** or **schedules**, or let an AI agent read plates through the **Apify MCP server**.

### Tips for best results

- The plate should be at least about **80 px wide** in the image; zoom the camera on the lane, not the whole street.
- Avoid strong motion blur: use a short shutter on gate cameras, or raise **Frames read per second** for fast traffic.
- At night, infrared cameras work well; headlight glare on the plate does not.
- Plates of other countries are detected, but the reader is tuned for Brazil and Mercosur. Turn off *Only plates in a known format* to see every read.

### FAQ

**Does it look up the owner or the vehicle registration?** No. It reads what is visible in your images. It does not query any registry or database.

**Which countries?** Brazil (old and Mercosur), Argentina, Paraguay and Uruguay layouts are validated. Other plates can be read with the format filter off, with lower accuracy.

**Can it read motorcycle plates (two rows)?** Yes, characters are ordered row by row.

**Can I use live RTSP camera streams?** Not directly: send recorded clips or periodic snapshots (e.g. every few seconds from your NVR, scheduled through the API).

**Is my footage kept?** Files are processed inside your run and stored only in your own Apify storage, under your account's data-retention settings. Nothing is sent to third parties.

### Responsible use

License plates are personal data under **LGPD** (Brazil) and **GDPR** (EU). Process only images you are allowed to process, for a legitimate purpose (access control, security, logistics, claims), keep results only as long as needed, and inform people where the law requires it (e.g. signs at camera-monitored entrances). Do not use this Actor to track or profile individuals.

### Related Actors

- [CCTV Motion Clipper](https://apify.com/adorable_partial/cctv-motion-clipper) — cut long recordings down to the moments with movement, then read plates only there
- [Image & Video Anonymizer](https://apify.com/adorable_partial/image-anonymizer) — blur faces and plates before sharing footage
- [Video Frame Extractor for AI](https://apify.com/adorable_partial/video-frame-extractor-ai) — frames for datasets and inspection
- [Image Quality Checker](https://apify.com/adorable_partial/image-quality-checker) — find blurry or dark camera snapshots

### Support

A plate read wrong? Open an issue in the **Issues** tab with the image (or a crop) and the correct text: it helps improve the models.

### Need a different tool? Request it

Missing a feature, an input or output format, or a whole different tool for your workflow? **Open an issue in the Issues tab** and describe what you need:

- what goes in (files, URLs, data) and what should come out,
- roughly how much you process (per day or month),
- one example, if you can share it.

Requests with a real use case get built, either as a new option in this Actor or as a new Actor in [this collection](https://apify.com/adorable_partial), and you get notified in the issue when it's live. Every request is read.

# Actor input Schema

## `imageUrls` (type: `array`):

Photos or camera snapshots (JPEG, PNG, WebP, BMP, TIFF; up to 50 MB each). Plate close-ups and plate crops work too. Public Google Drive and Dropbox links work.

## `videoUrls` (type: `array`):

CCTV, NVR/DVR exports, dashcam or phone videos (MP4, MKV, AVI, MOV, TS…; up to 4 GB each). You get one row per plate seen, with the time it appeared and left.

## `framesPerSecond` (type: `string`):

How many frames per second are read in videos. 2 (default) is enough for gates, parking and city traffic; use 4–5 for fast highway traffic. Higher = slower runs.

## `maxDurationMinutes` (type: `integer`):

Only the first N minutes of each video are read and billed.

## `includeVehicle` (type: `boolean`):

Add the vehicle that carries each plate: type, make and model (when confident) and color.

## `savePlateImages` (type: `boolean`):

Store a crop of every plate read (and of the vehicle, for videos) and link it in the results.

## `onlyValidPlates` (type: `boolean`):

Keep only reads that match a known layout (Brazil old ABC1234 and Mercosur ABC1D23, Argentina, Paraguay/Uruguay). Turn off to also get partial or foreign reads.

## `minTextConfidence` (type: `string`):

Average character confidence (0–1) a read needs to be returned. Raise it (e.g. 0.8) for fewer, surer reads.

## `minPlateConfidence` (type: `string`):

Detector confidence (0–1) a plate needs before it is read.

## Actor input object example

```json
{
  "imageUrls": [
    "https://thumb.wikimedia.org/wikipedia/commons/thumb/5/5b/Retardado_%288162343794%29.jpg/1920px-Retardado_%288162343794%29.jpg",
    "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/19/Incompetente_%288162346086%29.jpg/1920px-Incompetente_%288162346086%29.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/2/28/Placa_de_ve%C3%ADculo_Brasil_Pernambuco-Jaboat%C3%A3o_dos_Guararapes_KLV-8465_atr%C3%A1s.jpg"
  ],
  "framesPerSecond": "2",
  "maxDurationMinutes": 120,
  "includeVehicle": true,
  "savePlateImages": false,
  "onlyValidPlates": true,
  "minTextConfidence": "0.5",
  "minPlateConfidence": "0.25"
}
```

# Actor output Schema

## `results` (type: `string`):

Photos: one row per image with every plate read. Videos: one row per plate seen (vehicle passing) with first and last time.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "imageUrls": [
        "https://thumb.wikimedia.org/wikipedia/commons/thumb/5/5b/Retardado_%288162343794%29.jpg/1920px-Retardado_%288162343794%29.jpg",
        "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/19/Incompetente_%288162346086%29.jpg/1920px-Incompetente_%288162346086%29.jpg",
        "https://upload.wikimedia.org/wikipedia/commons/2/28/Placa_de_ve%C3%ADculo_Brasil_Pernambuco-Jaboat%C3%A3o_dos_Guararapes_KLV-8465_atr%C3%A1s.jpg"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("adorable_partial/license-plate-reader").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "imageUrls": [
        "https://thumb.wikimedia.org/wikipedia/commons/thumb/5/5b/Retardado_%288162343794%29.jpg/1920px-Retardado_%288162343794%29.jpg",
        "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/19/Incompetente_%288162346086%29.jpg/1920px-Incompetente_%288162346086%29.jpg",
        "https://upload.wikimedia.org/wikipedia/commons/2/28/Placa_de_ve%C3%ADculo_Brasil_Pernambuco-Jaboat%C3%A3o_dos_Guararapes_KLV-8465_atr%C3%A1s.jpg",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("adorable_partial/license-plate-reader").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "imageUrls": [
    "https://thumb.wikimedia.org/wikipedia/commons/thumb/5/5b/Retardado_%288162343794%29.jpg/1920px-Retardado_%288162343794%29.jpg",
    "https://thumb.wikimedia.org/wikipedia/commons/thumb/1/19/Incompetente_%288162346086%29.jpg/1920px-Incompetente_%288162346086%29.jpg",
    "https://upload.wikimedia.org/wikipedia/commons/2/28/Placa_de_ve%C3%ADculo_Brasil_Pernambuco-Jaboat%C3%A3o_dos_Guararapes_KLV-8465_atr%C3%A1s.jpg"
  ]
}' |
apify call adorable_partial/license-plate-reader --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adorable_partial/license-plate-reader"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Dt8KIjoN6utks8ern/builds/DoIcY5Nz5IHnWSW6x/openapi.json
