TingPush Camera AI

Integration docs

TingPush data integration guide for developers

Bản tiếng Việt

The starting point for teams building software (parking, buildings, factories, HR & time attendance, retail…) that needs data from TingPush cameras: licence plates, AI events (intrusion, faces, people / vehicle counting, over-capacity, custom models), time attendance, and control (opening barriers, plate allow lists, employee lists). Read this page first, then the detailed document for each part.


1. Choosing an integration path

A TingPush camera sends data one of two ways (the camera owner chooses 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 (Data destination)):

 (A) Camera ──pushes immediately (HTTP POST)──────────────────────►  Your server  (Own API)

 (B) Camera ──► TingPush server (stored per plan) ◄──pull (API v1, API key)──  Your software
                       │
                       └──► app / web / Windows software: viewing, Excel export, phone alerts
(A) Camera pushes to your own API (B) Pull from TingPush via API v1
Data Plates + time attendance Plates, AI events, counts, attendance, timesheets, employees, peripherals, plate lists
Latency Immediate (under 1 second after reading) Whatever interval you poll at (usually 5–30 seconds)
You need An HTTP endpoint the camera can reach (Internet or the site's local network) Outbound Internet; no ports to open
Storage You store it TingPush stores it per plan (viewing, export and app alerts keep working)
Network loss The camera queues (up to 300 records) and resends The camera resends to TingPush; you keep pulling from your last since_id — nothing is lost
Docs Writing an API that receives plate reads from TingPush cameras, Face time attendance — a TingPush camera as a time clock section 5 TingPush public API — fetching stored plate data and this page

Which to choose:

  • On-site parking software that must open the barrier as soon as a plate is read → (A). Or let the camera open the barrier itself from the plate allow list (your software doesn't have to decide — Peripherals — opening barriers, roller doors, sirens and lights from a TingPush camera) while data goes through (B).
  • Reports, reconciliation, accounting, HR & payroll, dashboards → (B): no receiving server to run, no data loss, and your customer can still see everything in the app.
  • AI events and counts only travel path (B) — the camera sends them to TingPush to alert phones.
  • A camera has one destination for plates / attendance. To get both (immediate delivery and storage on TingPush): choose (B) and call API v1 every 5 seconds, or contact TingPush.

No programming needed: Excel (CSV) export in the app / web, or Excel / Power BI Get data from web (section 9).


2. Quick start (10 minutes)

  1. Claim the camera into the account (app → Thêm camera / Add camera) and leave Data destination = TingPush server.
  2. The account needs a plan with API access (app → Tài khoản (Account) → Gói dịch vụ (Service plan)).
  3. Create an API key: app → Tài khoản → Khóa API (API keys) → Tạo khóa (Create key), or on the web at https://webrtc.tingpush.com/account. Keys look like tp_ + 40 characters and are shown once. Tick Điều khiển thiết bị & sửa danh sách xe (Control devices & edit plate lists) if your software must open barriers or edit plate / employee lists.
  4. Try it:
curl -H "X-Api-Key: tp_xxx" https://apiwebrtc.tingpush.com/api/v1/cameras
curl -H "X-Api-Key: tp_xxx" "https://apiwebrtc.tingpush.com/api/v1/plates?limit=5"
  1. Create test data: drive a vehicle past the camera, or press Gửi thử (Send test, sample plate 30A-123.45) / Thử cảnh báo (Test alert, a sample AI event with test: true) in the app.
  2. Write the sync loop from section 6.

3. Authentication, limits, errors

Base URL:     https://apiwebrtc.tingpush.com/api/v1
Auth:         X-Api-Key: tp_…          (or Authorization: Bearer tp_…)
Rate limit:   120 requests / minute / key → over the limit: 429, wait for the next minute
Format:       JSON UTF-8, snake_case field names
  • A key sees every camera the account can see (cameras it owns + cameras shared with it). Plain keys are read-only; only keys with control permission may call the changing commands (section 5, Perm. column).
  • Up to 10 keys per account. Give each piece of software / each site its own key — deleting a key disables it at once without affecting others. The key name appears in the peripheral log (API: <key name>).
  • Call from your server; never embed the key in a web page or an app you ship to users (a leaked key exposes all the data). The API does not allow CORS from other websites.
  • Images (*_image_url, photo_url) also need the API key header — download them to your server and serve them from there; don't hand those URLs to browsers.
Code Meaning What to do
200 / 204 Success (204: deleted, no content)
400 Bad parameters / data — {"error":"bad_request","message":"…"} Fix the data, don't resend it unchanged
401 Missing / wrong key, deleted key, or the plan has no API access — {"error":"unauthorized",…} Stop and alert an administrator
403 Read-only key used for a change, no permission on that camera, or the plan doesn't allow export Use a control key / upgrade the plan
404 No such record / image (or no permission to see that camera)
429 Over 120 requests / minute Wait 60 seconds, then continue
503 Camera offline / device reported an error (commands sent to a camera: opening a barrier…) Retry later; message explains
other 5xx Temporary server error Retry with increasing delays

Error messages (message / title) are in Vietnamese and can be shown to users.


4. Data conventions

Time time (ISO 8601 in Vietnam time, e.g. 2026-10-16T10:02:54+07:00) + timestamp (UNIX seconds, use this to store / compare). received_at = when the server received it — if the camera was offline and resent later, received_at is later than time.
Time parameters from, to in ISO 8601, range [from, to). URL-encode the +: from=2026-10-16T00:00:00%2B07:00 (a bare + becomes a space → 400). Timesheets take dates as yyyy-MM-dd.
Camera id LPR- + 12 characters (e.g. LPR-9MHK902C8J5Z), fixed for the camera's lifetime — use it as the camera key. camera_name can change.
Record id Integer, increasing in the order the server received records, never changes — your sync key. Plates, events and attendance each have their own id sequence.
Paging Without since_id: newest first; next page with before_id = the smallest id you received. With since_id: oldest first (sync), returns next_since_id + has_more. limit 1–1000 (default 100).
Plates plate is normalised (upper-case letters + digits, no punctuation, e.g. 51G12345) for storing / matching; plate_display for showing (51G-123.45). Filters accept any spelling.
Retention Records + images past the plan's retention are deleted automatically (attendance has its own retention, default 400 days) — sync to your system if you need them longer.
New fields Later versions may add fields — ignore unknown fields, don't validate against a fixed list.
Test data test: true (AI events) — still returned by the API; skip it in reports. Send test plates are not stored on TingPush.

5. API v1 catalogue

All under https://apiwebrtc.tingpush.com/api/v1. Perm.: R = a read-only key is enough, C = needs a key with control permission.

Cameras & plates — TingPush public API — fetching stored plate data

Call Perm.
GET /cameras R The account's cameras: id, name, role (owner / viewer), online, last_seen
GET /plates R Plates: filter camera, plate, from, to; sync since_id; before_id, limit
GET /plates/{id} R One record
GET /plates/{id}/image/plate · /image/scene R Plate crop / full scene (JPEG)
GET /export?format=csv (or json) R File export (up to 200,000 rows), same filters as /plates; the plan must allow export

AI events — AI events and alerts — people / vehicle counting, restricted-zone intrusion, face recognition

Call Perm.
GET /events R Events: filter camera, type (intrusion, face, occupancy, count, custom, io; comma-separated), from, to; since_id, before_id, limit
GET /events/{id}/image/scene · /image/crop R Full scene / object crop
GET /counts R Line-crossing counts: camera, from, to (default last 24 h, max 400 days), bucket = hour / day / month, by_line=true
GET /occupancy R People / vehicles currently inside (in − out) per camera

Time attendance & employees — Face time attendance — a TingPush camera as a time clock

Call Perm.
GET /attendance R Check-ins: filter camera, person, from, to; since_id, before_id, limit
GET /attendance/{id}/image/face · /image/scene R Face / full-scene image of a check-in
GET /attendance/timesheet?from=&to=&department= R Computed timesheet (up to 62 days, dates yyyy-MM-dd)
GET /employees R Employees: id, code, name, department, note, photo_url, cameras[]
GET /employees/{id}/photo R Registered portrait photo
POST /employees C Add an employee with a photo (JPEG base64) and cameras
PUT /employees/{id} C Edit (only fields sent); photo = replace photo; cameras = reset cameras
DELETE /employees/{id} C Delete (attendance history is kept)

Peripherals & plate lists — Peripherals — opening barriers, roller doors, sirens and lights from a TingPush camera section 6

Call Perm.
GET /cameras/{id}/outputs R Devices attached to the camera (barrier, roller door, siren…) + state
POST /cameras/{id}/outputs/{index}/trigger C Open (pulse) / on (on) / off (off)
GET /platelists R Plate allow lists
GET /platelists/{id}/entries?q=&offset=&limit= R Vehicles in a list
POST /platelists/{id}/entries C Add / update one vehicle (validity valid_from, valid_to)
DELETE /platelists/{id}/entries/{plate} C Delete one vehicle
POST /platelists/{id}/import C Merge / replace a whole list from CSV

6. Syncing data into your system

The standard pattern for every type with since_id (/plates, /events, /attendance):

for each type (plates, events, attendance):
    last_id = read from your database (0 the first time)
    loop:
        GET /<type>?since_id={last_id}&limit=500
        store each record — UNIQUE on id (skip ids you already have)
        (optional) download the images into your storage
        last_id = next_since_id     ← saved in the SAME transaction as the records
        has_more = true → call again now; false → wait 5–30 seconds
  • Nothing missed, nothing duplicated: id follows the order the server received records, including records the camera resent after being offline.
  • Save last_id in the same transaction as the records: if your software stops half-way it resumes from the old position and the UNIQUE id prevents duplicates.
  • Three types × once every 10 seconds = 18 requests / minute plus images — well within 120 / minute. If you download many images, space them out or fetch images only when a user opens a record.
  • On 429 → wait 60 seconds; 5xx / network error → wait 5 → 10 → 20 → 60 seconds and retry; 401 → stop and alert an administrator.
  • Store time / timestamp (when it happened) for reports and received_at to monitor delays.

Python — complete sync (plates, events, attendance + images)

Runs as is (Python 3.9+, pip install requests) and stores into SQLite — replace the storage part with your database.

"""Sync TingPush data into a database: plates, AI events, attendance (with images)."""
import json, os, sqlite3, time
import requests

API = "https://apiwebrtc.tingpush.com/api/v1"
KEY = os.environ["TINGPUSH_KEY"]          # tp_…
IMG_DIR = "images"                          # image folder; None = don't download images
s = requests.Session()
s.headers["X-Api-Key"] = KEY

db = sqlite3.connect("tingpush.db")
db.executescript("""
CREATE TABLE IF NOT EXISTS sync_state (kind TEXT PRIMARY KEY, last_id INTEGER NOT NULL);
CREATE TABLE IF NOT EXISTS plates (id INTEGER PRIMARY KEY, camera_id TEXT, plate TEXT, plate_display TEXT,
    ts INTEGER, allowed INTEGER, raw TEXT);
CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY, camera_id TEXT, type TEXT, summary TEXT, ts INTEGER,
    test INTEGER, raw TEXT);
CREATE TABLE IF NOT EXISTS attendance (id INTEGER PRIMARY KEY, camera_id TEXT, person_id TEXT, employee_code TEXT,
    name TEXT, ts INTEGER, source TEXT, raw TEXT);
""")

def get(path, **params):
    """GET with retries: 429 waits 60 s, network errors / 5xx back off."""
    delay = 5
    while True:
        try:
            r = s.get(f"{API}{path}", params=params, timeout=30)
            if r.status_code == 429:
                time.sleep(60); continue
            if r.status_code >= 500:
                raise requests.HTTPError(f"HTTP {r.status_code}")
            r.raise_for_status()           # 400 / 401 / 403 / 404: needs a human, don't retry
            return r
        except (requests.ConnectionError, requests.Timeout, requests.HTTPError) as e:
            if isinstance(e, requests.HTTPError) and e.response is not None and e.response.status_code < 500:
                raise
            print("Temporary error:", e, "- retrying in", delay, "seconds")
            time.sleep(delay); delay = min(delay * 2, 60)

def save_image(url, name):
    if not url or not IMG_DIR:
        return None
    path = os.path.join(IMG_DIR, name)
    if not os.path.exists(path):
        os.makedirs(IMG_DIR, exist_ok=True)
        with open(path, "wb") as f:
            f.write(get(url.replace(API, "")).content)
    return path

def store(kind, x):
    raw = json.dumps(x, ensure_ascii=False)
    if kind == "plates":
        db.execute("INSERT OR IGNORE INTO plates VALUES (?,?,?,?,?,?,?)",
                   (x["id"], x["camera_id"], x["plate"], x["plate_display"], x["timestamp"],
                    None if x.get("allowed") is None else int(x["allowed"]), raw))
        save_image(x.get("plate_image_url"), f"plate_{x['id']}.jpg")
    elif kind == "events":
        db.execute("INSERT OR IGNORE INTO events VALUES (?,?,?,?,?,?,?)",
                   (x["id"], x["camera_id"], x["type"], x["summary"], x["timestamp"], int(x.get("test", False)), raw))
        save_image(x.get("crop_image_url"), f"event_{x['id']}.jpg")
    elif kind == "attendance":
        db.execute("INSERT OR IGNORE INTO attendance VALUES (?,?,?,?,?,?,?,?)",
                   (x["id"], x.get("camera_id"), x.get("person_id"), x.get("employee_code"), x["name"],
                    x["timestamp"], x["source"], raw))
        save_image(x.get("face_image_url"), f"attendance_{x['id']}.jpg")

def sync(kind):
    row = db.execute("SELECT last_id FROM sync_state WHERE kind=?", (kind,)).fetchone()
    last_id = row[0] if row else 0
    while True:
        page = get(f"/{kind}", since_id=last_id, limit=500).json()
        with db:                                   # one transaction: records + sync position
            for x in page["data"]:
                store(kind, x)
            last_id = page["next_since_id"]
            db.execute("INSERT OR REPLACE INTO sync_state VALUES (?,?)", (kind, last_id))
        if page["data"]:
            print(kind, "+", len(page["data"]), "→ last_id", last_id)
        if not page["has_more"]:
            return

while True:
    for kind in ("plates", "events", "attendance"):
        sync(kind)
    time.sleep(10)

Drop the types you don't use from ("plates", "events", "attendance"). Without attendance in the plan, /attendance returns an empty list.

C# (.NET 8+) — background sync

using System.Net;
using System.Net.Http.Json;
using System.Text.Json;

var http = new HttpClient { BaseAddress = new Uri("https://apiwebrtc.tingpush.com/api/v1/"), Timeout = TimeSpan.FromSeconds(30) };
http.DefaultRequestHeaders.Add("X-Api-Key", Environment.GetEnvironmentVariable("TINGPUSH_KEY"));

var lastIds = new Dictionary<string, long> { ["plates"] = 0, ["events"] = 0, ["attendance"] = 0 }; // read from your database

while (true)
{
    foreach (var kind in lastIds.Keys.ToList())
    {
        bool more;
        do
        {
            using var res = await http.GetAsync($"{kind}?since_id={lastIds[kind]}&limit=500");
            if (res.StatusCode == HttpStatusCode.TooManyRequests) { await Task.Delay(TimeSpan.FromMinutes(1)); more = true; continue; }
            res.EnsureSuccessStatusCode();
            var page = await res.Content.ReadFromJsonAsync<JsonElement>();
            foreach (var x in page.GetProperty("data").EnumerateArray())
            {
                // store in your database — UNIQUE on id (INSERT … WHERE NOT EXISTS / MERGE)
                Console.WriteLine($"{kind} #{x.GetProperty("id")} {x.GetProperty("time")}");
            }
            lastIds[kind] = page.GetProperty("next_since_id").GetInt64(); // save in the same transaction as the records
            more = page.GetProperty("has_more").GetBoolean();
        } while (more);
    }
    await Task.Delay(TimeSpan.FromSeconds(10));
}

Images: await http.GetByteArrayAsync(x.GetProperty("plate_image_url").GetString()) (the same HttpClient, which already carries the key header).

Node.js (18+)

const API = 'https://apiwebrtc.tingpush.com/api/v1';
const H = { 'X-Api-Key': process.env.TINGPUSH_KEY };
const last = { plates: 0, events: 0, attendance: 0 }; // read from your database
const sleep = ms => new Promise(ok => setTimeout(ok, ms));

for (;;) {
  for (const kind of Object.keys(last)) {
    for (;;) {
      const r = await fetch(`${API}/${kind}?since_id=${last[kind]}&limit=500`, { headers: H });
      if (r.status === 429) { await sleep(60000); continue; }
      if (!r.ok) throw new Error(`${kind}: HTTP ${r.status} ${await r.text()}`);
      const page = await r.json();
      for (const x of page.data) console.log(kind, x.id, x.time); // store — UNIQUE on id
      last[kind] = page.next_since_id;                           // save in the same transaction
      if (!page.has_more) break;
    }
  }
  await sleep(10000);
}

PHP, Excel / Power BI: TingPush public API — fetching stored plate data section 4.

Data without since_id

Data How to get it
Counts per hour / day GET /counts?camera=…&from=…&to=…&bucket=hour — call per camera (no camera = all cameras added up), re-fetch the most recent window (e.g. the last 2 hours) each time and overwrite by (camera, line, start): the current hour keeps growing
People / vehicles inside GET /occupancy every 10–60 seconds (cameras update once per counting interval)
Timesheet GET /attendance/timesheet?from=…&to=… — recompute the whole range each time (manual entries and shift changes alter old results)
Employee list, plate lists Read when needed, or nightly

7. Receiving data pushed straight from the camera (Own API)

When the camera's Data destination is Own API, the camera POSTs JSON to your URL as soon as it reads a plate or records a check-in — same URL, told apart by type:

# FastAPI — one endpoint for both plates and attendance. pip install fastapi uvicorn
from fastapi import FastAPI, Header, HTTPException, Request

app = FastAPI()
SEEN = set()   # use a table with UNIQUE(event_id) in your database instead

@app.post("/api/tingpush")
async def receive(req: Request, authorization: str | None = Header(None)):
    if authorization != "Bearer your-secret":           # header the camera owner set in the app
        raise HTTPException(401)                          # 4xx: the camera drops the record, no resend
    x = await req.json()
    if x.get("test"):
        return {"ok": True}                               # "Send test" button: return 200, don't store
    if x["event_id"] in SEEN:
        return {"ok": True, "duplicate": True}            # resent after a lost reply: still 200
    if x["type"] == "plate":
        print("Plate", x["plate_display"], "camera", x["camera_id"], "allowed:", x.get("allowed"))
    elif x["type"] == "attendance":
        print("Check-in", x["employee_code"], x["name"], x["time"])
    # other types (later versions may add some): ignore, still return 200
    SEEN.add(x["event_id"])
    return {"ok": True}                                   # reply within 8 seconds; queue slow work

Required rules (details: Writing an API that receives plate reads from TingPush cameras sections 3, 6):

  • 2xx = received. 4xx = the camera drops the record. 5xx / timeout / connection error = the camera keeps it and resends. Temporary errors (busy database…) must return 503, not 4xx.
  • De-duplicate on event_id (UNIQUE key); duplicates still get 200.
  • Reply fast (the camera waits 8 seconds by default); record first, do heavy work (calling other systems, notifications) afterwards.
  • No redirects (3xx); configure the final URL.

Complete samples in C#, Node.js, PHP, Python and an API self-test tool: download the samples (test tool: kiem_tra_api.py, sample JSON: sample_request.json).


8. Writing data and control

Needs a key with Control devices & edit plate lists permission.

Open a barrier from parking software (e.g. a pay-per-visit vehicle has paid):

curl -X POST -H "X-Api-Key: tp_xxx" -H "Content-Type: application/json" -d '{"action":"pulse"}' \
     https://apiwebrtc.tingpush.com/api/v1/cameras/LPR-9MHK902C8J5Z/outputs/0/trigger

Sync monthly passes every night from your ticketing software — replace the whole list (cameras download the new version and keep opening for listed vehicles even offline):

curl -X POST -H "X-Api-Key: tp_xxx" -H "Content-Type: application/json" \
     -d '{"text":"51G12345,Nguyen Van A,01/10/2026,31/10/2026\n30A99999,Director car","replace":true}' \
     https://apiwebrtc.tingpush.com/api/v1/platelists/3/import

Sync employees from your HR software (face attendance):

import base64, requests
H = {"X-Api-Key": "tp_xxx"}
API = "https://apiwebrtc.tingpush.com/api/v1"
body = {"name": "Tran Thi B", "code": "NV002", "department": "Warehouse",
        "photo": base64.b64encode(open("NV002.jpg", "rb").read()).decode(),   # ID-style JPEG, looking straight
        "cameras": ["LPR-9MHK902C8J5Z"]}
r = requests.post(f"{API}/employees", json=body, headers=H, timeout=120)
emp = r.json()
bad = [c for c in emp.get("cameras", []) if c["state"] == "error"]   # e.g. "no face found" -> send another photo
print(r.status_code, emp.get("id"), bad)
  • The employee code is unique within the account — use it to match your system; keep the TingPush id returned to edit / delete later (PUT / DELETE /employees/{id}).
  • Someone leaves → DELETE /employees/{id}: cameras stop recognising them at once; attendance history is kept.
  • Adding an employee waits for the cameras to load the photo (a few seconds per camera): use an HTTP timeout ≥ 60 seconds.

9. Example scenarios

Car park with ticketing software

  1. Monthly passes: sync them into the plate allow list (section 8) → the camera opens the barrier itself, even offline.
  2. Pay-per-visit: your software reads plates (/plates every 2–5 seconds, or the camera pushes to your API), charges, then calls trigger to open the barrier.
  3. Reconciliation: store plate, time, the plate image; allowed tells you the camera recognised a listed vehicle.
  4. Barrier log (who opened it and why): GET /events?type=io.

HR & payroll (face attendance)

  1. Employees: create / edit / delete through /employees following your HR system.
  2. Check-ins: /attendance?since_id= → compute work time with your own shift rules (multiple shifts, overtime…).
  3. Or use TingPush's computed timesheet: /attendance/timesheet (one shift; late / early / absent).

Retail, buildings — footfall dashboard

  • GET /counts?bucket=hour for charts, GET /occupancy for the number inside, GET /events?type=occupancy for over-capacity alerts.

Monitoring centre — collecting alerts

  • GET /events?type=intrusion,face,custom&since_id= every 5 seconds; image: crop_image_url. The camera owner's phones are already notified / called by the app — your software is for operators and record keeping.

Excel / Power BI / Google Sheets — no code

  • Excel: Data → From Web → Advanced, URL https://apiwebrtc.tingpush.com/api/v1/export?format=csv&from=2026-10-01T00:00:00%2B07:00, add the X-Api-Key header.
  • Or download CSV in the app / web: Biển số → Xuất Excel (Plates → Export), Chấm công → Xuất CSV (Attendance → Export).

10. Go-live checklist

  • A dedicated API key for this software, kept in server configuration (not in source code, not in apps shipped to users).
  • last_id stored durably (database), in the same transaction as the records; tables have UNIQUE keys on id (API v1) / event_id (Own API).
  • 429 handled (wait 60 seconds), 5xx / network errors (retry with back-off), 401 (stop, alert an administrator).
  • from / to parameters encode + as %2B.
  • Unknown fields and unknown types are ignored; test: true is skipped in reports.
  • Images you need longer than the plan's retention are downloaded to your storage.
  • Offline test: cut the camera's network for 10 minutes, restore it — the resent data arrives complete, without duplicates.
  • Own API: run the kiem_tra_api.py test tool (correct status codes, de-duplication, large images).
  • Inform people who are filmed / clocked in, as personal data rules require (face images are sensitive data).

11. FAQ

Is there a webhook from TingPush to my server? Not yet — use Own API (the camera pushes immediately) or call API v1 with since_id every few seconds (a few seconds of delay, no data loss).

Do cameras shared with me appear in the API? Yes — a key sees every camera the account can see. Controlling devices also needs control permission on that camera.

Employee photos / check-in images? photo_url in /employees, face_image_url / image_url in /attendance — download them with the same API key header.

What if the camera changes owner? Old data belongs to the previous owner; the new owner only sees data from the moment they claimed the camera.

Test environment? Use a separate camera + account for testing; the Send test / Test alert buttons in the app create sample data without real vehicles or people.

Need help? Contact TingPush via the Contact page on the website — include the camera id (LPR-…), the time, the request and the error code.