TingPush data integration guide for developers
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)
- Claim the camera into the account (app → Thêm camera / Add camera) and leave Data destination = TingPush server.
- The account needs a plan with API access (app → Tài khoản (Account) → Gói dịch vụ (Service plan)).
- 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 liketp_+ 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. - 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"
- 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 withtest: true) in the app. - 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:
idfollows the order the server received records, including records the camera resent after being offline. - Save
last_idin the same transaction as the records: if your software stops half-way it resumes from the old position and the UNIQUEidprevents 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 andreceived_atto 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, not4xx. - De-duplicate on
event_id(UNIQUE key); duplicates still get200. - 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
codeis unique within the account — use it to match your system; keep the TingPushidreturned 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
- Monthly passes: sync them into the plate allow list (section 8) → the camera opens the barrier itself, even offline.
- Pay-per-visit: your software reads plates (
/platesevery 2–5 seconds, or the camera pushes to your API), charges, then callstriggerto open the barrier. - Reconciliation: store
plate,time, the plate image;allowedtells you the camera recognised a listed vehicle. - Barrier log (who opened it and why):
GET /events?type=io.
HR & payroll (face attendance)
- Employees: create / edit / delete through
/employeesfollowing your HR system. - Check-ins:
/attendance?since_id=→ compute work time with your own shift rules (multiple shifts, overtime…). - Or use TingPush's computed timesheet:
/attendance/timesheet(one shift; late / early / absent).
Retail, buildings — footfall dashboard
GET /counts?bucket=hourfor charts,GET /occupancyfor the number inside,GET /events?type=occupancyfor 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 theX-Api-Keyheader. - 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_idstored durably (database), in the same transaction as the records; tables have UNIQUE keys onid(API v1) /event_id(Own API). -
429handled (wait 60 seconds),5xx/ network errors (retry with back-off),401(stop, alert an administrator). -
from/toparameters encode+as%2B. - Unknown fields and unknown
types are ignored;test: trueis 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.pytest 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.