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
}
| Field | What it is |
|---|---|
familiarId | Your stable identifier. This is the value every read carries — pick it once and never change it |
familiarName | What operators see in grids and filters |
detectorType | Your engine's name and version |
timeZone | IANA 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 }
]
}
]
| Field | Why it matters |
|---|---|
plate | The read itself. Watch lists, similar-plate matching and alerts all key on it. The server removes spaces and truncates to its stored length |
detector | The LPR detector's familiarId |
dataTimeStamp | When the plate was read, UTC. Everything time-based works from this |
timeZone | The detector's IANA zone |
direction | Which way the vehicle was travelling, relative to the detector (see below) |
state | The issuing state or province, where your engine reads it |
make, model, color | Vehicle details where your engine produces them — they make a plate searchable by more than its characters |
images | The 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
| Value | Meaning |
|---|---|
0 | None — the detector cannot tell |
1 | In |
2 | Out |
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
| Value | Format |
|---|---|
0 | JPEG |
1 | PNG |
2 | GIF |
3 | BMP |
cropSizeId — how much of the scene
| Value | Crop |
|---|---|
0 | Close — the plate itself |
1 | Medium |
2 | Car — the whole vehicle |
3 | Full frame |
4 | Other |
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 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.
Related endpoints
| Method | Path | What it does |
|---|---|---|
POST | oa/plugins/{id}/license-plate-detections | Insert reads |
GET | oa/plugins/{id}/license-plate-detections | Query reads |
PUT | oa/plugins/{id}/license-plate-detectors | Register a detector |
GET | oa/plugins/{id}/license-plate-detectors | List detectors to map against |
GET | oa/plugins/{id}/license-plates | The plate records themselves |
GET | oa/plugins/{id}/license-plates/similar | Plates close to a given read |
GET | oa/plugins/{id}/license-plate-notify-tags | Watch lists a read can match |
GET | oa/plugins/{id}/license-plates/stats | Read 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.