TingPush Camera AI

Integration docs

Writing an API that receives plate reads from TingPush cameras

Bản tiếng Việt

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 (http vs https, with or without www, 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_image are 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) alongside timestamp.

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:

  1. Never return 4xx for temporary problems (database busy, another service not ready…) — return 503 and the camera resends. Return 4xx and the record is lost.
  2. Wrong token → 401 is 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.
  3. 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 200 immediately; slow work (calling other systems, opening the barrier, notifications…) goes to a queue / background job.
  4. 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.
  5. 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_id a UNIQUE key in your database.
  • When the event_id already exists → don't store it again, still return 200 (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

  1. Self-test with the tool (it plays the camera; only needs Python 3):
    python vi_du_api/kiem_tra_api.py https://api.company.com/api/plates --header "Authorization: Bearer abc123"
    
    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 plate 99Z-999.99, camera LPR-KIEMTRA00000 — delete them afterwards.
  2. 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.
  3. 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.