Writing an API that receives plate reads from TingPush cameras
For developers building a server that receives license plate data from TingPush cameras (RV1126BP-LPR).
Every time the camera confirms a plate, it sends an HTTP POST (JSON) straight to your URL.
All you need is one endpoint that accepts that request and returns 200.
Don't want to build an API? Let the camera send to the TingPush server (the default) and pull the data through the public API or file export: TingPush public API — fetching stored plate data. This document covers the own API option.
Ready-to-run samples: vi_du_api/ — C# (ASP.NET Core), Node.js (Express), PHP, Python (FastAPI).
API self-test tool: vi_du_api/kiem_tra_api.py.
The TingPush app and the camera's setup page are in Vietnamese. Screen labels are quoted as they appear, with an English translation in parentheses.
1. Overview
Camera ──(plate read, confirmed enough times)──► POST https://your-server/api/plates (JSON, optionally with images)
◄──────────────── 200 OK ──────────
- One URL per camera. The camera owner sets it in the TingPush Camera app → Cài đặt camera (Camera settings)
→ Gửi dữ liệu biển số (Send plate data) → Nơi nhận dữ liệu (Destination) = API riêng (Own API), or on the
camera's web setup page, Biển số & API tab. Several cameras may share one URL — tell them apart by
camera_id. - The camera calls your URL directly (not through the TingPush server). The URL can be an Internet address
(
https://api.company.com/...) or an address on the car park's local network (http://192.168.1.10:5000/...) — as long as the camera can reach it. - Each plate is sent once per vehicle visit (the camera suppresses repeats while a vehicle stays in front of it, 60 seconds by default).
- The camera only reads plates during the reading hours and inside the reading area set by the owner (app → Cài đặt đọc biển số, Plate reading settings).
- The camera has a persistent queue: if your server is down or unreachable, the camera keeps the records (up to 300) and resends them once it can connect again, even after a power cut.
2. The request the camera sends
POST /api/plates HTTP/1.1
Host: your-server
User-Agent: lpr-camera/1.0
Content-Type: application/json; charset=utf-8
Content-Length: 1234
Connection: close
Authorization: Bearer abc123 ← only when the owner sets it (the header name can change, e.g. X-Api-Key)
{ ...JSON below... }
- The method is always
POST; the body is always a JSON object, UTF-8 encoded. - The camera does not follow redirects (301/302/307/308). It treats them as temporary errors and keeps retrying —
configure the final URL exactly (
httpvshttps, with or withoutwww, with or without a trailing/). - HTTPS is supported. By default the camera does not verify certificates (self-signed certificates work); the owner can turn on Kiểm tra chứng chỉ HTTPS (Verify HTTPS certificate), which then requires a valid certificate (e.g. Let's Encrypt).
Example JSON
{
"event_id": "LPR-9MHK902C8J5Z-1792114200-000042",
"type": "plate",
"device_id": "CONG-VAO-1",
"camera_id": "LPR-9MHK902C8J5Z",
"plate": "59T184807",
"plate_display": "59T1-848.07",
"raw_text": "59T1-84807",
"confidence": 0.912,
"read_count": 3,
"time": "2026-10-16T08:30:00+07:00",
"timestamp": 1792114200,
"box": [1520, 980, 1810, 1105],
"frame_width": 3840,
"frame_height": 2160,
"image_type": "jpeg",
"image": "/9j/4AAQSkZJRgABAQ...",
"plate_image_type": "jpeg",
"plate_image": "/9j/4AAQSkZJRgABAQ..."
}
Without images: vi_du_api/sample_request.json.
Fields
| Field | Type | Always | Meaning |
|---|---|---|---|
event_id |
string | ✔ | Unique ID of this read. When the camera resends (because a response was lost), event_id stays the same → use it to drop duplicates (section 6). Format <camera_id>-<timestamp>-<sequence>, up to 100 characters — treat it as an opaque string, don't parse it. |
type |
string | ✔ | "plate" (licence plate) or "attendance" (face time attendance — a camera with attendance enabled sends to the same URL; format in Face time attendance — a TingPush camera as a time clock section 5). Other AI events (intrusion, faces, counting) are not sent to your API — they go to the TingPush server for alerts; fetch them via /api/v1/events (AI events and alerts — people / vehicle counting, restricted-zone intrusion, face recognition). If your API only handles plates, return 200 and ignore records whose type is not plate. |
camera_id |
string | ✔ | Permanent camera ID (LPR- + 12 characters). It never changes, even after reinstalling the firmware. Use it as the camera key. |
device_id |
string | ✔ | Name the installer sets on the setup page (e.g. CONG-VAO-1, "gate in 1"), defaults to the MAC address. Handy for display / lanes; it can change. |
plate |
string | ✔ | Normalised plate: uppercase letters and digits, no dashes, dots or spaces (e.g. 30A12345, 59T184807). Use it for storage, search and matching. |
plate_display |
string | ✔ | Plate formatted the Vietnamese way for display (e.g. 30A-123.45, 59T1-848.07). |
raw_text |
string | ✔ | Text exactly as the model read it (two-line plates joined with -). Useful when investigating misreads. |
confidence |
number | ✔ | Confidence of the best plate box, 0–1. |
read_count |
int | ✔ | How many times the camera read the same plate before confirming it (≥ the configured threshold, 3 by default). |
time |
string | ✔ | Confirmation time, ISO 8601 with the camera's time zone (e.g. +07:00). |
timestamp |
int | ✔ | The same moment as UNIX seconds (UTC). Prefer this field for storing and comparing. |
box |
int[4] | ✔ | Plate position in the frame [x1, y1, x2, y2] (pixels). |
frame_width, frame_height |
int | ✔ | Size of the frame box refers to (usually 3840×2160). |
image |
string | if enabled | Full scene JPEG, base64 (no data: prefix). Small (sub stream) ~50–150 KB, sharp (4K main stream) ~0.5–2 MB. |
image_type |
string | with image |
Always "jpeg". |
plate_image |
string | if enabled | Plate close-up colour JPEG, base64, ~5–20 KB, cropped from the full-resolution frame with a margin around the plate. |
plate_image_type |
string | with plate_image |
Always "jpeg". |
allowed |
bool | when the camera has a list | true = the plate is on the allow list (still valid), false = it isn't. Absent when no list is assigned to the camera — see Peripherals — opening barriers, roller doors, sirens and lights from a TingPush camera. |
allow_name |
string | when allowed = true |
Owner name / note from the list (or the list name). |
test |
bool | test only | true when the user taps Gửi thử (Send test) in the app / setup page (sample plate 30A-123.45). Return 200 but don't store it. |
Notes:
- Ignore unknown fields. Future firmware may add fields; never reject a request because of an unfamiliar one.
- Whether
image/plate_imageare present depends on the owner ticking Gửi ảnh toàn cảnh (Send scene image) / Gửi ảnh biển số (Send plate image) — your API must work without images. - The camera clock can be off if it hasn't synced yet (the app has a Đồng bộ giờ, Sync time, button). Store the
server receive time (
received_at) alongsidetimestamp.
3. Responding to the camera — the most important part
The camera only reads the HTTP status code, not the body (the body is only shown when the user taps Send test).
| You return | The camera understands | The camera does |
|---|---|---|
| 2xx (200, 201, 204) | Received | Removes the record from the queue, sends the next one |
| 4xx (400, 401, 403, 404, 413, 422…) | Permanently rejected | DROPS the record, never resends |
| 408, 429 | Temporary | Keeps it, resends later |
| 5xx, 3xx (redirect), timeout, connection lost, DNS / TLS error | Temporary | Keeps it, resends after 5 → 10 → 20 → 40 → 60 seconds (60 seconds max between tries) |
Consequences:
- Never return 4xx for temporary problems (database busy, another service not ready…) — return
503and the camera resends. Return4xxand the record is lost. - Wrong token →
401is correct (the camera drops it instead of retrying in vain) — but remember: vehicles passing while the token is wrong are not resent after you fix the token. - Respond quickly: the camera waits at most Thời gian chờ máy nhận (Receiver timeout; 8 seconds by default,
adjustable 5–60 seconds). After that it treats the request as failed and resends → you get a duplicate. Store
the record and return
200immediately; slow work (calling other systems, opening the barrier, notifications…) goes to a queue / background job. - The camera sends one record at a time, oldest first. A record failing with a temporary error blocks the ones after it until it goes through — keep the API stable and don't return 5xx for a particular record for long.
- Keep the response body short (e.g.
{"ok":true}); the camera reads at most 2 KB.
4. Request size
A 4K main-stream scene image can reach 2 MB → ~3 MB after base64. Some servers' defaults are too small (you get
413 → the camera drops the record). Allow ≥ 20 MB:
| Platform | Setting |
|---|---|
| nginx (reverse proxy) | client_max_body_size 20m; (the default is only 1 MB!) |
| PHP | post_max_size = 20M in php.ini |
| ASP.NET Core | KestrelServerLimits.MaxRequestBodySize (30 MB by default — enough); IIS: maxAllowedContentLength |
| Node.js Express | express.json({ limit: '20mb' }) (the default is 100 KB!) |
| Python FastAPI / Flask | no limit by default; behind nginx, see the nginx row |
| Cloudflare / API gateway | check the service's request body limit |
5. Authentication
The camera owner sets a header name (Authorization by default) and a value (e.g. Bearer abc123, or the
header X-Api-Key with the value abc123). The camera sends exactly that header on every request.
- Give each customer / car park its own token; matching the token tells you whose data it is.
- You can also filter by
camera_id: accept only the cameras registered for that token. - Use HTTPS whenever the URL crosses the Internet (the token and images travel in the request).
- Compare tokens with a constant-time function (
hash_equals,CryptographicOperations.FixedTimeEquals…).
6. Dropping duplicates (idempotency)
The camera guarantees at least once delivery: if you stored the record but the response never reached the camera
(flaky network, response slower than the timeout), the camera resends the same record with the same event_id.
So:
- Make
event_ida UNIQUE key in your database. - When the
event_idalready exists → don't store it again, still return200(returning an error makes the camera resend until it gives up).
-- PostgreSQL (MySQL: INSERT IGNORE / ON DUPLICATE KEY UPDATE id=id; SQL Server: MERGE or catch error 2627)
CREATE TABLE plate_events (
id BIGSERIAL PRIMARY KEY,
event_id VARCHAR(100) NOT NULL UNIQUE,
camera_id VARCHAR(32) NOT NULL,
device_id VARCHAR(64),
plate VARCHAR(24) NOT NULL,
plate_display VARCHAR(24),
raw_text VARCHAR(32),
confidence REAL,
read_count INT,
event_time TIMESTAMPTZ NOT NULL, -- from "timestamp"
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
box INT[],
image_path TEXT, -- path to the scene image file (don't store base64 in the database)
plate_image_path TEXT
);
CREATE INDEX ON plate_events (plate, event_time DESC);
CREATE INDEX ON plate_events (camera_id, event_time DESC);
INSERT INTO plate_events (event_id, camera_id, device_id, plate, plate_display, raw_text, confidence, read_count, event_time, box)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, to_timestamp($9), $10)
ON CONFLICT (event_id) DO NOTHING; -- duplicate: do nothing, still return 200
7. Storing images
- Decode the base64 → write a file (disk / S3 / MinIO…); keep only the path in the database.
- Name files by
event_id(e.g.LPR-9MHK902C8J5Z-1792114200-000042_plate.jpg) — not by time + plate: two cameras seeing the same vehicle in the same second would overwrite each other's images. - Plate images are ~10 KB per visit; small scene images ~100 KB, sharp ones up to 2 MB — plan your storage and set a retention period for old images.
8. Suggested processing steps
1. Check the authentication header → wrong: 401
2. Parse the JSON, check plate, timestamp → missing / malformed: 400
3. "test": true → 200, don't store
4. INSERT ... ON CONFLICT (event_id) DO NOTHING (+ write the image files)
database / disk error (temporary) → 503 (the camera resends)
5. Return 200 {"ok":true} — right away, within 1–2 seconds
6. Everything else (open the barrier, check monthly passes, notifications...) runs in the background after responding
All 4 samples in vi_du_api/ follow these steps (they write to a data/ folder so you can try them
without a database):
| Run | |
|---|---|
C# — vi_du_api/csharp |
dotnet run --urls http://0.0.0.0:5000 |
Node.js — vi_du_api/node |
npm install && node server.js |
PHP — vi_du_api/php/plates.php |
php -S 0.0.0.0:5000 plates.php (or upload to your hosting) |
Python — vi_du_api/python |
pip install fastapi uvicorn && uvicorn main:app --host 0.0.0.0 --port 5000 |
Set the environment variable API_TOKEN=abc123 to require the header Authorization: Bearer abc123.
9. Testing your API before go-live
- Self-test with the tool (it plays the camera; only needs Python 3):
It sends a Send test record, a record with scene + plate images, a duplicate resend, a request without the token, and a ~3 MB image, then reports ĐẠT / LỖI (PASS / FAIL) for each with how to fix it (the tool's messages are in Vietnamese). It creates 2 records for platepython vi_du_api/kiem_tra_api.py https://api.company.com/api/plates --header "Authorization: Bearer abc123"99Z-999.99, cameraLPR-KIEMTRA00000— delete them afterwards. - With a real camera: in the app → Cài đặt camera → Gửi dữ liệu biển số → enter the URL + token → Gửi thử (Send test). The app shows the HTTP status and the body you returned. The same screen shows the counts Đã gửi / Đang chờ / Lỗi (Sent / Pending / Failed) and the latest error.
- Test an outage: stop your API for a few minutes while vehicles pass, start it again → every record must arrive, in order, without duplicates.
10. Common problems
| Message (app → Send plate data) | Cause |
|---|---|
| "Không kết nối được …" (Cannot connect) | The camera can't reach the server: wrong IP/port, a firewall (Windows Firewall), or a local URL while the camera is on another network. |
| "Không phân giải được tên máy chủ (DNS)" (Cannot resolve host name) | Wrong domain, or the camera's network has no DNS. Try an IP address. |
| "Bắt tay TLS thất bại" (TLS handshake failed) | Verify HTTPS certificate is on and the certificate is self-signed / expired / for another domain. |
| "Máy chủ trả mã HTTP 401/403" (Server returned 401/403) | Wrong header name or token value (note the space in Bearer ). |
| "Máy chủ trả mã HTTP 404/405" | Wrong path, or the endpoint doesn't accept POST. |
| "Máy chủ trả mã HTTP 301/302/308" | The URL redirects (http→https, missing / extra trailing /) — set the final URL. |
| "Máy chủ trả mã HTTP 413" | Body size limit (section 4) — usually nginx client_max_body_size. |
| "Máy chủ không trả lời trong N giây" (No response within N seconds) | The API is slow — respond first, process later (section 3). |
| Duplicate records | Normal on flaky networks — drop duplicates by event_id (section 6). |
| Pending keeps growing | The API is returning 5xx / timing out; the camera keeps the data and catches up once you fix it. |
11. Versions
This format applies to camera firmware that includes event_id / plate_image (October 2026 onwards). Older
firmware lacks those two fields — without event_id, drop duplicates by camera_id + timestamp + plate.