Skip to main content

Pushing licence plate reads

If your LPR camera or engine is the one reading plates, push the reads into Object Analytics and the whole module works on them: plate search, plate tags and watch lists, similar-plate matching, alerts at the gate, and the video of the vehicle arriving.

POST /api/v2/oa/plugins/{pluginInstalledId}/license-plate-detections

The body is an array, so this is the batch endpoint as well as the single one. It returns true on success.

Register a detector first​

An LPR detector is the camera or engine that read the plate, as eConnect knows it. Every read names its detector by familiar id, and a read whose detector does not exist has nowhere to land. Create it once, at install time:

PUT /api/v2/oa/plugins/{pluginInstalledId}/license-plate-detectors
{
"familiarId": "acme-lpr-gate-1",
"familiarName": "North Gate — Inbound",
"detectorType": "Acme LPR 4.0",
"timeZone": "America/New_York",
"isDisabled": false
}
FieldWhat it is
familiarIdYour stable identifier. This is the value every read carries — pick it once and never change it
familiarNameWhat operators see in grids and filters
detectorTypeYour engine's name and version
timeZoneIANA zone (America/New_York), so local-time reporting is right regardless of where the server sits

Lanes are usually one detector each. A gate that reads inbound and outbound on separate cameras is two detectors, not one with a direction flag — that way a grid filtered to a lane is filtered to a camera.

List what exists with GET .../license-plate-detectors, map your ids once at start-up, and fail loudly on anything unmapped. If you ever register a duplicate, an administrator can merge it with POST .../license-plate-detectors/{targetDetectorId}/merge and the reads move with it.

The payload​

[
{
"plate": "7ABC123",
"dataTimeStamp": "2026-10-02T21:14:05Z",
"detector": "acme-lpr-gate-1",
"timeZone": "America/New_York",
"direction": 1,
"state": "NV",
"make": "Toyota",
"model": "Tacoma",
"color": "White",
"images": [
{ "imageData": "/9j/4AAQSkZJRgABAQAAAQ...", "imageTypeId": 0, "cropSizeId": 0 },
{ "imageData": "/9j/4AAQSkZJRgABAQAAAQ...", "imageTypeId": 0, "cropSizeId": 2 }
]
}
]
FieldWhy it matters
plateThe read itself. Watch lists, similar-plate matching and alerts all key on it. The server removes spaces and truncates to its stored length
detectorThe LPR detector's familiarId
dataTimeStampWhen the plate was read, UTC. Everything time-based works from this
timeZoneThe detector's IANA zone
directionWhich way the vehicle was travelling, relative to the detector (see below)
stateThe issuing state or province, where your engine reads it
make, model, colorVehicle details where your engine produces them — they make a plate searchable by more than its characters
imagesThe pictures. See below

Send the characters your engine actually read, not a cleaned-up version: eConnect does its own similar-plate matching and it works better on the raw read. A misread O for 0 is still found by similar plates; a "corrected" plate that was corrected wrongly is not.

Direction​

ValueMeaning
0None — the detector cannot tell
1In
2Out

Send 0 rather than guessing. A direction that is wrong half the time is worse than one that is absent, because reports will quietly believe it.

Images​

Each read carries any number of images, each tagged with its format and how tightly it is cropped. imageData is the raw image bytes, base64-encoded.

imageTypeId — the format

ValueFormat
0JPEG
1PNG
2GIF
3BMP

cropSizeId — how much of the scene

ValueCrop
0Close — the plate itself
1Medium
2Car — the whole vehicle
3Full frame
4Other

Tag them honestly: an operator reviewing an alert wants the plate crop to confirm the read and the vehicle shot to recognise the car, and the module picks which to show by this value. Sending every image as 0 means the grid shows a thumbnail of a number plate where a car should be.

Images make batches heavy

Images are base64 inside the body, so a two-hundred-row batch with three images each is a very large request. Keep plate batches small — twenty or so — and send JPEG unless you have a reason not to.

Sending it​

curl -X POST \
"https://your-server/api/v2/oa/plugins/$OA_ID/license-plate-detections" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '[
{
"plate": "7ABC123",
"dataTimeStamp": "2026-10-02T21:14:05Z",
"detector": "acme-lpr-gate-1",
"timeZone": "America/New_York",
"direction": 1,
"state": "NV"
}
]'
var reads = batch.Select(r => new
{
plate = r.RawText, // as read, not cleaned up
dataTimeStamp = r.SeenUtc,
detector = detectorFamiliarId,
timeZone = "America/New_York",
direction = (byte)(r.Inbound ? 1 : 2), // 0 = unknown
state = r.Region,
make = r.Make,
model = r.Model,
color = r.Colour,
images = new[]
{
new { imageData = r.PlateJpeg, imageTypeId = 0, cropSizeId = 0 }, // the plate
new { imageData = r.VehicleJpeg, imageTypeId = 0, cropSizeId = 2 }, // the car
},
}).ToArray();

var response = await http.PostAsJsonAsync(
$"oa/plugins/{oaId}/license-plate-detections", reads);
response.EnsureSuccessStatusCode();
import base64, requests

read = {
"plate": r.text,
"dataTimeStamp": r.seen_utc.isoformat().replace("+00:00", "Z"),
"detector": DETECTOR_ID,
"timeZone": "America/New_York",
"direction": 1,
"state": r.region,
"images": [
{"imageData": base64.b64encode(r.plate_jpeg).decode(), "imageTypeId": 0, "cropSizeId": 0},
{"imageData": base64.b64encode(r.car_jpeg).decode(), "imageTypeId": 0, "cropSizeId": 2},
],
}

resp = requests.post(
f"{BASE}/oa/plugins/{oa_id}/license-plate-detections",
headers={"Authorization": f"Bearer {token}"},
json=[read], # a list, even for one
timeout=60,
)
resp.raise_for_status()

Associating a plate with a person​

A plate read stands on its own, but eConnect can also tie a plate to a subject, so a face at the door and a vehicle at the gate are the same visit:

PUT /api/v2/oa/plugins/{pluginInstalledId}/subjects/{subjectId}/license-plates/{licensePlateId}
DELETE /api/v2/oa/plugins/{pluginInstalledId}/subjects/{subjectId}/license-plates/{licensePlateId}
GET /api/v2/oa/plugins/{pluginInstalledId}/subject-license-plate-associations

This is usually an operator's judgement rather than something an ingress feed should assert. Push the reads; let the association be made deliberately.

MethodPathWhat it does
POSToa/plugins/{id}/license-plate-detectionsInsert reads
GEToa/plugins/{id}/license-plate-detectionsQuery reads
PUToa/plugins/{id}/license-plate-detectorsRegister a detector
GEToa/plugins/{id}/license-plate-detectorsList detectors to map against
GEToa/plugins/{id}/license-platesThe plate records themselves
GEToa/plugins/{id}/license-plates/similarPlates close to a given read
GEToa/plugins/{id}/license-plate-notify-tagsWatch lists a read can match
GEToa/plugins/{id}/license-plates/statsRead volumes

The full list is on the Object Analytics reference page.

Permissions​

The service account needs the ObjectAnalytics* keys for the operations it performs — the plate detection insert, plus the detector keys if it registers its own detector at install time — and the Object Analytics plugin instance must be within its logical group. An account that only pushes reads does not need the tag, association or removal keys. See the Permission Keys reference.

What operators see​

Pushed reads are ordinary reads. They are found by Licence Plate Search including its similar-plate matching, match plate tags and raise the alerts configured on them, build up a plate profile of everywhere that vehicle has been seen, and — where the detector is associated with a camera — open the footage of the vehicle arriving.