Connecting clock-in machines to Tarleaks ERP
ZKTeco and eSSL (ADMS), Hikvision, the on-site bridge for every other machine, the punch API with examples, and log-file imports — and what is never stored.
Last updated
This guide is for the IT person, integrator or HR administrator who connects fingerprint, face and card machines to Tarleaks ERP. Every machine page in Tarleaks links here from its Connection tab.
Who this is for, and the 60-second version
Machines sold in the UAE and the Gulf do not share one connection standard. Tarleaks therefore has one secure door for punches — a punch API over HTTPS, with a key per machine — and five ways to reach it. Find your machine in the table and follow its section.
| Your machine | What to do | Section |
|---|---|---|
| ZKTeco, eSSL or a similar brand whose menu has COMM → Cloud Server Setting (or ADMS) | Type our address into the machine's menu. Nothing to install. | ZKTeco and eSSL (ADMS) |
| Hikvision access terminal (set up from its web page) | Set its HTTP host to our address. Nothing to install. | Hikvision |
| A ZKTeco-compatible machine on your office network (TCP port 4370), a Hikvision terminal with no internet access, older ZKTeco firmware that only speaks http, or any brand whose software saves its log to a shared folder | Install the small bridge program on a PC at the site. | The on-site bridge |
| Any other brand | Import its exported file, or have its software call the punch API. | Importing a machine's file, The punch API reference |
| Your own software, or an integrator's | Call the punch API. | The punch API reference |
| A machine that can only save its log to a USB stick | Import the file from time to time. | Importing a machine's file |
A wall tablet running the Tarleaks kiosk is not covered here: pair it in Settings → Attendance → Kiosk.
How it works
Every route ends in the same place: machine → (direct connection, the bridge, a file, or your own code) → the punch inbox of that machine in Tarleaks → matching people (which employee is machine user 1001?) → clock-ins on each person's time record, flagged for review where something looks wrong.
What is stored for each punch: which machine, the machine's own user number for the person, the time, in or out (when the machine says), how the person was verified (fingerprint, face, card, PIN, palm), the address it came from and how it arrived. For each machine user: the number, the name on the machine and a card number.
What is never stored, never requested and never read: fingerprint templates, face templates, photos, temperature and mask readings, user passwords and the machine's own passwords. ZKTeco machines are told not to send templates or photos; anything a machine sends anyway is thrown away unread. This follows the UAE Personal Data Protection Law (PDPL).
How long it is kept:
| Data | Kept for |
|---|---|
| Punches that became clock-ins, double reads and punches waiting for review | 24 months by default (Settings → Attendance → Clock-in machines → Advanced: 6 to 120 months) |
| Punches nobody was matched to, punches set aside, punches refused | 90 days |
| The sender's address and the raw machine line kept with each punch | removed after 90 days |
| Machines that tried to connect but were never added | 30 days |
| Finished machine commands | 90 days |
| Machine users never matched and not seen for the punch period | deleted |
The clock-ins themselves are ordinary time records and follow your normal attendance retention.
Before you start
- Your workspace address. It is
https://<company>.tarleaks.com— the address you open Tarleaks with. On a self-hosted installation, it is your installation's own address (TARLEAKS_URLin its.env), and the machine's Connection tab shows it with the right port. If that address is plainhttp://on your office network, the steps say so and the machine's HTTPS switch stays off. - Who can do it. Adding and changing machines needs the permission attendance — device — write (the Tenant admin and HR manager roles have it). Matching machine users to employees needs device punch — resolve (Tenant admin, HR manager, HR specialist). Importing a log file needs device punch — import (Tenant admin, HR manager), because it creates clock-ins for other people. Managers can see the machines and whether they are connected.
- Switch it on. Settings → Features → Attendance → Clock-in machines must be on (it is on by default). When it is off, machines are refused and their punches are not taken in; machines already added stay listed, and you can still archive a machine or replace its key.
- Allow machine clock-ins. Settings → Attendance → Allowed sources must include Clock-in machines. Without it, punches arrive but wait for review instead of becoming clock-ins.
- Time zone. Machines keep local time. Tarleaks reads a machine's times in the machine's time zone: the one set on the machine's page, else the default under Settings → Attendance → Clock-in machines → Advanced, else your workspace's time zone. Set the machine's own clock to the same zone.
- The network. Machines and the bridge only make outgoing HTTPS connections on port 443 to your workspace address. Nothing has to be opened inbound on your firewall. Plain http (port 80) is not served — a key must never cross the internet unencrypted. (A self-hosted installation inside your office network follows its own address: its machines use whatever scheme and port the Connection tab shows.)
Then go to Attendance → Clock-in machines → Add a machine. The wizard asks what kind of machine it is, its name and work location, and shows the machine's connection key once, followed by the exact steps for that machine.
Security model
The connection key. Each machine gets its own key: atd_ followed by 48 letters and digits (52 characters). It is shown once, when the machine is added or the key is replaced. Tarleaks stores only a one-way hash (SHA-256) of it, so nobody — including us — can show it again. The first 12 characters (for example atd_k3v7q2ma) stay visible on the machine's page so you can tell keys apart. Issue a new key replaces it instantly: the old key stops working on the next request. Archiving a machine also stops its key.
Protect the machine's menu. A ZKTeco machine shows its server address — and so its key — in its menu. Set an administrator password on the machine (Menu → User Mgt → make one user Administrator) so nobody can read the key off it. On Hikvision, set a strong web password.
The key address and the serial-number address. ZKTeco machines normally use the key address (<company>.tarleaks.com/zk/<key>). Some older models only have a server-address field that cannot take a path; for those there is a serial-number address (just <company>.tarleaks.com). Because a serial number is printed on the box and is not a secret, the serial-number address is off by default, and when you turn it on it works only for a serial number an administrator typed in and only from the internet addresses you allowed. In short: serial-only means anyone who knows the serial AND is at an address you allowed. Use the key address whenever the machine accepts it.
Allowed addresses. Any machine can be limited to your office's internet address(es): on the machine's page, Connection → Allowed addresses (single addresses or ranges such as 94.56.10.0/24; up to 20). A request from any other address is refused. When a request using a serial-number machine's serial comes from an address you have not allowed, its page lists every such address (up to 5, with how many tries and when). Anyone who knows a serial number can send such a request, so only the address you are using yourself is offered with one click; any other address must be typed again to confirm. Nothing is ever allowed automatically.
HTTPS only. Every machine route is served on HTTPS only. An older machine that cannot do HTTPS connects through the bridge's ADMS relay on your network instead.
Limits. Each machine may make 120 requests a minute, where a request with many punches counts more (1 + 1 for every 50 punches). Each internet address may make 300 requests a minute with at most 20 at the same time. Each machine may send at most 50,000 new punches and 5,000 new machine users a day, and a workspace 500,000 new punches a day. Over a limit the answer is 429 with a Retry-After header; nothing is lost by waiting.
What is logged. Every batch received is written to the attendance audit log with the machine and the sender's address. Server logs record the route, never the address with the key in it.
Machines trying to connect. A ZKTeco machine pointed at the serial-number address before it was added appears on Attendance → Clock-in machines under "machines are trying to connect", with its serial number and address. To add it you type the serial number from the machine's own label or System Info screen — it is never filled in for you.
ZKTeco and eSSL (ADMS)
Works with ZKTeco, eSSL and other machines whose firmware offers Cloud Server Setting or ADMS with HTTPS. Menu names differ slightly between models and firmware versions.
- Add the machine in Tarleaks: Attendance → Clock-in machines → Add a machine → ZKTeco, eSSL or similar. Copy the connection key when it is shown.
- On the machine, set an administrator password (Menu → User Mgt → make one user Administrator), so nobody can read the connection key off its menu.
- On the machine: Menu → COMM → Cloud Server Setting (on some models: Comm. → ADMS).
- Turn Enable Domain Name ON.
- Server Address:
<company>.tarleaks.com/zk/<key>— your workspace address, then/zk/, then the machine's connection key. The whole address is about 80 characters (acme-uae.tarleaks.com/zk/atd_…is 77). The machine adds/iclock/…to it by itself. - Server Port: 443.
- HTTPS: ON. Proxy: OFF.
- Menu → System → Date/Time: set the time zone shown on the machine's Connection tab (for the UAE: UTC+04:00).
- Save, then restart the machine. Its first contact shows on the machine's page within a minute.
Other address forms. The long form <company>.tarleaks.com/api/attendance/public/v1/adms/<key> works the same way. If the Server Address field will not take a path at all, open the machine's page → Advanced → tick My machine only has a server-address field, type the machine's serial number (from its label or Menu → System Info), allow your office's internet address, and set Server Address to just <company>.tarleaks.com.
Good to know.
- The machine must trust our certificate (a public Let's Encrypt certificate) and speak TLS 1.2. Some older firmware does not; those machines connect through the bridge's ADMS relay.
- Addresses ending in
.aspx(eSSL firmware) are handled. - The machine's Comm Key is not used by this connection.
- A machine keeps its punches until Tarleaks answers "OK", so a refused or failed upload loses nothing — the machine simply tries again.
- On the first connection the machine sends the punches it holds; punches from before the date you chose under "Bring in punches from" are kept but set aside.
Hikvision
Works with Hikvision access-control and face terminals that can send events to an HTTP host (ISAPI "HTTP Listening" or "Alarm Server").
- Add the machine in Tarleaks: Attendance → Clock-in machines → Add a machine → Hikvision terminal. Copy the connection key.
- Set a strong administrator password on the terminal's web page.
- On the terminal's web page: Configuration → Network → Advanced → HTTP Listening (on some models: Alarm Server).
- Protocol: HTTPS. Address type: domain name. Host:
<company>.tarleaks.com. Port: 443. - URL:
/api/attendance/public/v1/hikvision/<key>/events— put the machine's connection key where<key>is. - Format: JSON. Authentication: none (the key in the address is the credential).
- Event → Access Control → linkage: tick Upload to center (or Notify surveillance center) for authentication events.
- System → Time: NTP server
pool.ntp.org, time zone GMT+04:00 (or your zone). - Save and press Test. The first event shows on the machine's page.
For integrators, the same setting through ISAPI:
PUT /ISAPI/Event/notification/httpHosts
Content-Type: application/xml
<HttpHostNotificationList version="2.0" xmlns="http://www.isapi.org/ver20/XMLSchema">
<HttpHostNotification>
<id>1</id>
<url>/api/attendance/public/v1/hikvision/atd_REPLACE_WITH_THE_KEY/events</url>
<protocolType>HTTPS</protocolType>
<parameterFormatType>JSON</parameterFormatType>
<addressingFormatType>hostname</addressingFormatType>
<hostName>yourcompany.tarleaks.com</hostName>
<portNo>443</portNo>
<httpAuthenticationMethod>none</httpAuthenticationMethod>
</HttpHostNotification>
</HttpHostNotificationList>
Which events count as a punch. An access event (major type 5) whose minor code is 1, 38, 75 or 113 and that carries an employee number becomes a punch. Other access events — a door refusal, for example — are kept as "not a pass event" so you can see them; to count more codes, add them on the machine's page → Advanced → accepted event codes. Heartbeats are recorded as contact only. The terminal's attendanceStatus (check in, check out, break, overtime) is kept as in/out. Pictures, temperature and mask readings are never stored.
A Hikvision terminal that cannot reach the internet can be read by the bridge instead (adapter = hik_isapi).
The punch API reference
The same API the bridge uses. Anything that can make an HTTPS request — your own software, an integration platform, a script — can send punches with it.
Base address: https://<company>.tarleaks.com/api/attendance/public/v1
Authentication. Add a machine of the kind Our own software / an integrator to get a connection key, then send it on every request:
Authorization: Bearer atd_k3v7q2ma…
(X-Attendance-Device-Token: atd_… is also accepted; prefer Authorization, which HTTP clients never forward to another host.) Do not follow redirects: a correct address never redirects, and an HTTP client that follows one may send your key elsewhere.
Endpoints
| Method and path | Request body | Answer (200) |
|---|---|---|
GET /device | — | the machine, its time zone and in/out rule, the workspace name, the server time and your limits |
POST /punches | a batch of 1 to 500 punches (at most 512 KB) | a report: accepted, duplicates, a result per punch, refused punches |
POST /device-users | 1 to 2,000 machine users (at most 1 MB) | {"stored": n, "new": m} |
GET /commands | — | commands waiting for this machine (they are then marked as sent) |
POST /commands/{id}/result | {"ok": true, "message": "412 punches re-sent"} (at most 16 KB) | {"recorded": true} |
GET /device answers:
{
"device": {"id": "8c1f…", "name": "Main entrance", "connection_type": "api",
"time_zone": "Asia/Dubai", "direction_policy": "first_last"},
"workspace": "ACME UAE",
"server_time": "2026-10-11T04:01:30Z",
"limits": {"max_batch": 500, "per_minute": 120, "per_day": 50000}
}
Sending punches
{
"punches": [
{
"person": "1001",
"time": "2026-10-11 08:01:22",
"direction": "in",
"verify": "fingerprint",
"card": "0012345678",
"id": "zk1-778812"
}
]
}
| Field | Required | Meaning |
|---|---|---|
person | yes, 1–64 characters | Who punched: the machine's own user number by default. |
time | yes | YYYY-MM-DD HH:MM:SS or YYYY-MM-DDTHH:MM:SS, read in the machine's time zone (recommended — send what the machine shows), or RFC 3339 with an offset such as 2026-10-11T08:01:22+04:00 when you know the exact moment. |
person_kind | no | device_user (default), employee_number (the person's employee number in Tarleaks) or badge (their badge or card number). |
direction | no | in, out, break_out, break_in, overtime_in, overtime_out or unknown. |
status_code | no | The machine's raw state number, kept for reference. |
verify | no | fingerprint, face, card, pin, palm or other. |
work_code | no, up to 32 | The machine's work code, if used. |
card | no, up to 64 | The card number read, if any. |
id | no, 1–160 | Your own unique id for the punch. Not needed: the person and the time are the key anyway. |
Unknown fields are ignored. Send the machine's own local time, not your server's: the machine's time zone in Tarleaks is the one authority, so the same punch sent by two routes is recognised as one.
The answer:
{
"accepted": 1,
"duplicates": 0,
"results": [{"index": 0, "id": "zk1-778812", "status": "processed", "punch_id": "5b0e…"}],
"rejected": [{"index": 3, "id": null, "code": "BAD_TIME", "message": "time is not a date and time"}],
"server_time": "2026-10-11T04:01:30Z"
}
index is the punch's position in your batch (from 0). One bad punch never fails the batch: it is listed in rejected with a code — BAD_PERSON, BAD_TIME, BAD_DIRECTION, BAD_ID, TOO_LONG or BAD_FIELD (a field of the wrong type, for example an object where text belongs; the message names the field) — and the rest are stored. A number is accepted wherever text is expected ("person": 1001 reads as "1001"). Each stored punch has a status:
| status | Meaning |
|---|---|
received | Stored; it becomes a clock-in within a few minutes. |
processed | Already part of a clock-in. |
unmatched | Stored, but nobody is matched to this machine user yet (Attendance → Machine users). |
needs_review | Stored, but waiting for a person (see the reason codes in the appendix). |
ignored | Stored and set aside (for example, from before the machine's start date). |
duplicate_read | The same person punched again within the double-read window; kept, not counted. |
duplicate | You sent this punch before; nothing changed. |
rejected | Stored but refused, for example a time more than 5 minutes in the future (check the machine's clock). |
Resending is always safe. The key of a punch is its person and its time, so resending a batch after a timeout, a crash or a 503 can never create a second clock-in. When in doubt, send it again.
Errors
Errors use one shape: {"error": {"code": "…", "message": "…"}}.
| HTTP | code | What it means — what to do |
|---|---|---|
| 400 | BAD_BATCH | Not JSON, or punches missing, empty or more than 500. Fix the body. (A wrongly typed field inside one punch is not a batch error: that punch is listed in rejected as BAD_FIELD.) |
| 401 | DEVICE_TOKEN_INVALID | The key is missing, wrong, replaced or the machine was archived. Copy the current key, or issue a new one on the machine's page. |
| 402 | ATTENDANCE_NOT_LICENSED | The workspace has no Attendance licence. Ask your Tarleaks contact. |
| 403 | WORKSPACE_SUSPENDED | The workspace is suspended. |
| 403 | DEVICE_INTEGRATIONS_OFF | Settings → Features → Attendance → Clock-in machines is off (or switched off by the platform). Turn it on. |
| 403 | DEVICE_INACTIVE | The machine is inactive or archived. |
| 403 | DEVICE_ADDRESS_NOT_ALLOWED | The machine has allowed addresses and this request came from another one. Allow it on the machine's page. |
| 403 | WRONG_WORKSPACE_ADDRESS | The key belongs to another workspace than the address you called. Use your own workspace address. |
| 408 | TRY_AGAIN | The request body took more than 15 seconds to arrive. Nothing was stored; send the same batch again after Retry-After seconds (a slow link: send smaller batches). |
| 413 | BODY_TOO_LARGE | Send smaller batches. |
| 429 | SLOW_DOWN or DAILY_LIMIT | Too many requests or punches. Wait the number of seconds in Retry-After, then send the same batch again. |
| 503 | TRY_AGAIN | Nothing was stored. Wait Retry-After seconds and send the same batch again. |
On a network error or any other 5xx, wait and retry with a growing delay (for example 5 seconds doubling up to 10 minutes). On 401, 402 or 403, stop and fix the cause; retrying will not help.
Machine users and commands
POST /device-users with {"users": [{"person": "1001", "name": "Sara Ali", "card": "0012345678"}]} tells Tarleaks the names on the machine, which makes matching people easier (only person is required). Send only these three fields — never fingerprints, faces or passwords.
GET /commands returns what an administrator asked the machine to do from its page:
| kind | payload | What your code should do |
|---|---|---|
sync_time | — | Set the machine's clock to the current time in its zone. |
repull_history | {"from": "2026-10-01", "to": "2026-10-11"} | Send every punch between those dates again (resending is safe). |
fetch_users | — | Send the machine's user list to POST /device-users. |
reboot | — | Restart the machine. |
Report each one with POST /commands/{id}/result and {"ok": true|false, "message": "…"}.
The developer reference inside Tarleaks (Developer → API reference) and its OpenAPI file at https://<company>.tarleaks.com/api/auth/developer/openapi.json describe the signed-in API used to manage machines; the machine endpoints above are documented here.
Examples
Each example does the same four things: checks the connection, sends a batch, reads the report, and retries on 429 and 503 using Retry-After. Keep the key in an environment variable (TARLEAKS_KEY) or a file, never in the code.
curl (bash):
BASE="https://yourcompany.tarleaks.com/api/attendance/public/v1"
KEY="$(cat /etc/tarleaks/entrance.key)" # atd_…
# 1. Check the connection
curl -sS --proto =https --max-redirs 0 -H "Authorization: Bearer $KEY" "$BASE/device"
# 2-4. Send a batch, retrying on 429 / 503 / 5xx with Retry-After
cat > batch.json <<'JSON'
{"punches": [{"person": "1001", "time": "2026-10-11 08:01:22", "direction": "in", "verify": "fingerprint"}]}
JSON
for attempt in 1 2 3 4 5 6 7 8; do
code=$(curl -sS --proto =https --max-redirs 0 -o report.json -D headers.txt -w '%{http_code}' \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
--data-binary @batch.json "$BASE/punches") || code=000
case "$code" in
200) cat report.json; break ;;
429|5??|000)
wait=$(awk 'tolower($1)=="retry-after:" {print $2+0}' headers.txt)
sleep "${wait:-$((5 * attempt))}" ;;
*) echo "Refused ($code):"; cat report.json; break ;;
esac
done
Python (3.9+, requests):
import os
import time
import requests
BASE = "https://yourcompany.tarleaks.com/api/attendance/public/v1"
session = requests.Session()
session.headers["Authorization"] = "Bearer " + os.environ["TARLEAKS_KEY"]
def call(method, path, body=None, tries=8):
for attempt in range(tries):
backoff = min(600, 5 * 2 ** attempt)
try:
r = session.request(method, BASE + path, json=body, timeout=30, allow_redirects=False)
except requests.RequestException:
time.sleep(backoff)
continue
if r.status_code == 429 or r.status_code >= 500:
retry_after = r.headers.get("Retry-After", "")
time.sleep(int(retry_after) if retry_after.isdigit() else backoff)
continue
data = r.json() if r.content else {}
if r.status_code != 200:
err = data.get("error", {})
raise RuntimeError(f"{r.status_code} {err.get('code')}: {err.get('message')}")
return data
raise RuntimeError("Gave up for now; sending the same batch again later is safe.")
print(call("GET", "/device")["workspace"])
report = call("POST", "/punches", {"punches": [
{"person": "1001", "time": "2026-10-11 08:01:22", "direction": "in", "verify": "fingerprint"},
]})
print(report["accepted"], "accepted,", report["duplicates"], "duplicates")
for bad in report["rejected"]:
print("punch", bad["index"], "refused:", bad["code"], bad["message"])
Node.js (18+, built-in fetch):
const BASE = "https://yourcompany.tarleaks.com/api/attendance/public/v1";
const KEY = process.env.TARLEAKS_KEY;
const sleep = (s) => new Promise((done) => setTimeout(done, s * 1000));
async function call(method, path, body) {
for (let attempt = 0; attempt < 8; attempt++) {
const backoff = Math.min(600, 5 * 2 ** attempt);
let res;
try {
res = await fetch(BASE + path, {
method,
redirect: "manual",
headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(30_000),
});
} catch {
await sleep(backoff);
continue;
}
if (res.status === 429 || res.status >= 500) {
await sleep(Number(res.headers.get("retry-after")) || backoff);
continue;
}
const data = await res.json().catch(() => ({}));
if (res.status !== 200) {
throw new Error(`${res.status} ${data.error?.code}: ${data.error?.message}`);
}
return data;
}
throw new Error("Gave up for now; sending the same batch again later is safe.");
}
const info = await call("GET", "/device");
console.log(`Connected to ${info.workspace} as ${info.device.name}`);
const report = await call("POST", "/punches", {
punches: [{ person: "1001", time: "2026-10-11 08:01:22", direction: "in", verify: "fingerprint" }],
});
console.log(`${report.accepted} accepted, ${report.duplicates} duplicates`);
for (const bad of report.rejected) console.log(`punch ${bad.index} refused: ${bad.code}`);
(Save it as send.mjs, or use it inside an async function.)
C# (.NET 6+, HttpClient):
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var http = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false })
{
BaseAddress = new Uri("https://yourcompany.tarleaks.com/api/attendance/public/v1/"),
Timeout = TimeSpan.FromSeconds(30),
};
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("TARLEAKS_KEY"));
var json = new JsonSerializerOptions(); // keep the field names exactly as written
async Task<JsonElement> Call(HttpMethod method, string path, object? body = null)
{
for (var attempt = 0; attempt < 8; attempt++)
{
var backoff = TimeSpan.FromSeconds(Math.Min(600, 5 * Math.Pow(2, attempt)));
HttpResponseMessage res;
try
{
var req = new HttpRequestMessage(method, path);
if (body != null) req.Content = JsonContent.Create(body, options: json);
res = await http.SendAsync(req);
}
catch (Exception e) when (e is HttpRequestException || e is TaskCanceledException)
{
await Task.Delay(backoff);
continue;
}
var code = (int)res.StatusCode;
if (code == 429 || code >= 500)
{
await Task.Delay(res.Headers.RetryAfter?.Delta ?? backoff);
continue;
}
var text = await res.Content.ReadAsStringAsync();
if (code != 200)
throw new Exception($"{code}: {text}");
return JsonDocument.Parse(text).RootElement;
}
throw new Exception("Gave up for now; sending the same batch again later is safe.");
}
var info = await Call(HttpMethod.Get, "device");
Console.WriteLine($"Connected to {info.GetProperty("workspace")}");
var report = await Call(HttpMethod.Post, "punches", new
{
punches = new[] { new { person = "1001", time = "2026-10-11 08:01:22", direction = "in", verify = "fingerprint" } },
});
Console.WriteLine($"{report.GetProperty("accepted")} accepted, {report.GetProperty("duplicates")} duplicates");
Note the paths have no leading / ("punches"), so they add to the base address.
PowerShell (7+):
$Base = "https://yourcompany.tarleaks.com/api/attendance/public/v1"
$Headers = @{ Authorization = "Bearer $env:TARLEAKS_KEY" }
function Invoke-Tarleaks([string]$Method, [string]$Path, $Body = $null) {
for ($attempt = 0; $attempt -lt 8; $attempt++) {
$backoff = [Math]::Min(600, 5 * [Math]::Pow(2, $attempt))
$params = @{ Method = $Method; Uri = "$Base$Path"; Headers = $Headers; TimeoutSec = 30
MaximumRedirection = 0; SkipHttpErrorCheck = $true }
if ($null -ne $Body) {
$params.Body = $Body | ConvertTo-Json -Depth 5
$params.ContentType = "application/json; charset=utf-8"
}
try { $r = Invoke-WebRequest @params } catch { Start-Sleep -Seconds $backoff; continue }
$code = [int]$r.StatusCode
if ($code -eq 429 -or $code -ge 500) {
$wait = 0
if (-not [int]::TryParse([string]($r.Headers["Retry-After"] | Select-Object -First 1), [ref]$wait)) { $wait = $backoff }
Start-Sleep -Seconds $wait
continue
}
$data = if ($r.Content) { $r.Content | ConvertFrom-Json } else { $null }
if ($code -ne 200) { throw "$code $($data.error.code): $($data.error.message)" }
return $data
}
throw "Gave up for now; sending the same batch again later is safe."
}
$info = Invoke-Tarleaks GET "/device"
"Connected to $($info.workspace) as $($info.device.name)"
$report = Invoke-Tarleaks POST "/punches" @{ punches = @(
@{ person = "1001"; time = "2026-10-11 08:01:22"; direction = "in"; verify = "fingerprint" }
) }
"$($report.accepted) accepted, $($report.duplicates) duplicates"
$report.rejected | ForEach-Object { "punch $($_.index) refused: $($_.code)" }
The on-site bridge
Download the on-site bridge
Version 0.1.0 for Windows, Linux and Docker. Needs Python 3.9 or newer.
Download tarleaks-attendance-bridge-0.1.0.zip (157 KB)
SHA-256 checksum — compare it before installing:
b0bb10f9f81fe53218175718c16991908f13b005bc4e08a97cc85061d68cb0ba tarleaks-attendance-bridge-0.1.0.zipWindows: Get-FileHash .\tarleaks-attendance-bridge-0.1.0.zip -Algorithm SHA256
Linux / macOS: sha256sum tarleaks-attendance-bridge-0.1.0.zipReading ZKTeco machines over the network (adapter zk_tcp) also needs the optional pyzk package, licensed GPL-2.0, which you install yourself from the pinned file in the package; it is not included in the download.
The bridge is a small program for a PC at your site that stays on. It reads machines on your office network and sends their punches through the punch API above. It speaks to four kinds of source only — the table below; a machine of another brand reaches it only through a folder its own software saves the log into.
Download. The bridge package is a zip file published with its SHA-256 checksum on the public copy of this guide, tarleaks.com/developers/attendance-devices. Check the checksum before installing. It needs Python 3.9 or newer.
What it reads (one [machine:<name>] section per machine in bridge.ini):
| adapter | For | Settings |
|---|---|---|
zk_tcp | ZKTeco, eSSL, FingerTec on TCP port 4370 | address, port (4370), comm_key_env or comm_key_file if the machine has a Comm Key, full_read_hours (6). Needs the optional pyzk package: pip install --require-hashes -r requirements-zk.txt. pyzk is licensed GPL-2.0; you install it yourself and it is never bundled with the bridge. |
hik_isapi | Hikvision terminals that cannot reach the internet | address, username, password_env or password_file, poll_seconds (60) |
adms_relay | Older ZKTeco firmware that can only push over plain http | listen (this PC's own LAN address and a port, never 0.0.0.0), serial (the one machine accepted), machine_ips (its address) |
folder | USB exports (attlog.dat, .txt) or CSV files dropped in a folder — from any brand whose software saves its log | path, format (attlog or csv), attlog_layout, csv_columns, date_order |
Each machine needs its own connection key: add it in Tarleaks as Another brand, or an older machine on our office network.
attlog_layout (folder, format = attlog). Log files put the in/out state in different columns. A ZKTeco / FingerTec USB export (1_attlog.dat) reads person, time, machine number, state, verify, work code: set attlog_layout = usb. A file in the order person, time, state, verify, work code is attlog_layout = adms. Left at auto, the bridge tells from each file (a USB export has six columns and the same machine number on every line); a file that cannot tell — one line, or every punch the same state — is moved to failed\ with a note instead of being read the wrong way round. Set the layout and move the file back into the folder.
Install on Windows. Unzip the package, open PowerShell as administrator in that folder and run:
powershell -ExecutionPolicy Bypass -File .\attendance_bridge\service\install-windows.ps1
It uses only what Windows already has: it creates a Python environment with hash-checked packages, a dedicated local account, a settings folder (C:\ProgramData\TarleaksBridge) that only that account and administrators can open, and a start-up task. Then, in a new PowerShell window:
tarleaks-attendance-bridge init --url https://yourcompany.tarleaks.com --machine entrance --adapter zk_tcp --address 192.168.1.201
tarleaks-attendance-bridge test
Start-ScheduledTask -TaskName "Tarleaks Attendance Bridge"
Install on Linux (systemd): follow the steps at the top of attendance_bridge/service/tarleaks-attendance-bridge.service — a system account, a Python environment, the settings in /etc/tarleaks-bridge, then systemctl enable --now tarleaks-attendance-bridge.
Docker: attendance_bridge/service/Dockerfile (non-root; mount bridge.ini and the key files read-only). The ADMS relay needs --network host.
Keys and passwords stay out of bridge.ini. init asks for the key without showing it and saves it in a key file only the bridge's account can read (token_file). You can use an environment variable instead (token_env = TARLEAKS_KEY_ENTRANCE). On Windows the key file sits in C:\ProgramData\TarleaksBridge, which the installer opens to the bridge's own account and administrators only. (token_dpapi is read too, but a DPAPI file opens only for the Windows account that wrote it — never write one as an administrator for the start-up task, whose account is TarleaksBridge; test warns when you do.) Machine passwords and Comm Keys work the same way (password_file / password_env, comm_key_file / comm_key_env). The bridge refuses to start if it finds a key or password written in bridge.ini.
A full example bridge.ini:
[server]
url = https://yourcompany.tarleaks.com
; ca_bundle = C:\certs\company-proxy.pem ; only behind a proxy that inspects HTTPS
[machine:entrance]
adapter = zk_tcp
token_file = C:\ProgramData\TarleaksBridge\keys\entrance.key
address = 192.168.1.201
port = 4370
[machine:warehouse]
adapter = hik_isapi
token_env = TARLEAKS_KEY_WAREHOUSE
address = 192.168.1.64
username = admin
password_env = HIK_WAREHOUSE_PASSWORD
[machine:old-zk]
adapter = adms_relay
token_env = TARLEAKS_KEY_OLDZK
listen = 192.168.1.10:8080
serial = CJDE203760123
machine_ips = 192.168.1.205
[machine:usb-exports]
adapter = folder
token_env = TARLEAKS_KEY_USB
path = C:\AttendanceExports
format = attlog
The ADMS relay. Point the old machine's Cloud Server / ADMS setting at the bridge PC (192.168.1.10, port 8080, HTTPS off). The relay listens only on that address, accepts only the configured serial number from the configured machine address, refuses anything sent by a web browser, answers the machine's start-up exchange itself (asking for punches and user records only), answers "OK" only after the punches are safely queued, and forwards them to Tarleaks over HTTPS.
Check it. tarleaks-attendance-bridge test checks the settings, the secure connection ("Connected to ACME UAE as 'Entrance'") and reads each machine once without sending anything. tarleaks-attendance-bridge status shows what is waiting to be sent and each machine's last problem. Logs are in logs\bridge.log next to the settings file (journalctl -u tarleaks-attendance-bridge on Linux); keys never appear in them.
What it sends, and never sends. It sends the punch fields listed in the API reference and the machine user list (number, name, card). It never reads or sends fingerprint or face templates, photos, user passwords or the Comm Key.
When the internet is down. Punches wait in a local queue and are sent when the connection is back — nothing is lost, and resending is harmless. On 429 the bridge waits as told; on a network error it retries from 5 seconds up to every 10 minutes; on 401, 402 or 403 it stops that machine, writes the reason in plain words in the log and tries again every 10 minutes. On 400 or 413 the server refused what was sent, not the connection: the bridge splits the batch until the one punch the server will not take is alone, sends everything else, and after that punch is refused on its own twice sets it aside with an ERROR line naming the person and the time — so one damaged record never holds up the punches behind it. status counts the punches set aside; correct the record (or the file) on the machine and use Fetch punches again to send it. Every 10 minutes it checks in, warns when the machine's clock is more than 2 minutes off, and carries out commands sent from the machine's page (set the time, fetch punches again, read the user list, restart). A punch the server has already answered for is not sent again for 7 days (Fetch punches again clears that memory).
When the machine's clock moves back (a time-zone change, or Set the machine's time after it ran ahead), punches keep arriving: the bridge never drops a punch because its time is older than the last one read. It notices the machine's clock is behind the last punch read, says so in the log, and reads the last day again; punches already sent are not sent twice. The bridge carries its own time-zone data (tzdata, in requirements.txt), so the clock check and Set the machine's time use the machine's zone even on Windows.
Updating. Download the new package, check its SHA-256, replace the app folder (Windows: C:\Program Files\TarleaksBridge\app), and restart the task or service. Settings, keys and the queue are kept.
Importing a machine's file
For machines that only save their log to a USB stick, or when you need to load history:
- Export the log. ZKTeco and similar: put a USB stick in the machine, Menu → USB Manager (or Pen Drive Mng) → Download → Attendance Data. You get a file such as
1_attlog.dat. Other machines: export the attendance log as CSV or Excel from the machine or its software. - Import it. On the machine's page in Tarleaks (add one of the kind It only exports a file), choose Import a log file or spreadsheet (on its Punches tab), pick the file, check the columns in the preview — the column order of USB files differs between models, so the first rows are shown for you to confirm — and import.
- Time zone. Times in the file are read in the machine's time zone unless you choose another while importing. An in/out column is optional.
Importing the same file again is safe: punches already stored are reported as duplicates. Importing needs the device punch — import permission, because it creates clock-ins for the people in the file; the person who imported is recorded on every punch.
Matching people
A machine knows people by its own user numbers. Tarleaks has to know which employee is machine user 1001.
- When adding a machine, the wizard asks "Is each person's ID on this machine their employee number?". If you answer yes, Tarleaks matches automatically when a machine user number equals an employee number (also with leading zeros removed:
0042=42) or when a card number equals an employee's badge number. - It holds back when the machine reports a name for that user and the name has nothing in common with the employee's name (Arabic and English spellings are compared fairly). Such cases become suggestions instead.
- Attendance → Machine users lists every machine user. The Not matched filter shows who still needs a match and how many punches are waiting for them. Choose Match, pick the employee (or create one on the spot) and keep Same person on all machines ticked when the same number is used on every machine. The waiting punches turn into clock-ins straight away.
- Automatic matches show as "matched automatically" with Confirm and Change. A match made by a person always wins over an automatic one.
- Match automatically on that page runs the rules once, on request, even when automatic matching is off.
- Changing or removing a match affects future punches only; clock-ins already made stay as they are.
How punches become clock-ins
The in/out rule (Settings → Attendance → Clock-in machines; a machine can override it):
| Rule | How it works | Example: punches at 07:58, 13:02, 13:40, 17:05 |
|---|---|---|
| First and last punch of the day (default) | The first punch of the work day is the check-in, the last is the check-out. | one session 07:58–17:05 |
| The machine's in/out button | Uses the in/out the person chose on the machine. | two sessions 07:58–13:02 and 13:40–17:05 (if pressed correctly) |
| Alternate | In, out, in, out… regardless of buttons. | two sessions 07:58–13:02 and 13:40–17:05 |
Most machines in the region are set up without the in/out button, which is why "first and last" is the default. If every punch shows as "in", switch to first and last.
- Double reads. A second punch by the same person within 60 seconds (10 to 600, Settings) is a double read: kept, not counted.
- Longest shift and forgotten check-outs. A session longer than the longest shift (16 hours by default, 4 to 24) is never created. A check-in with no check-out by then becomes a zero-minute entry flagged missing check-out; a check-out with no check-in is flagged missing check-in. The person fixes it with a correction request, like any other.
- Work days and night shifts. A punch belongs to the work day of the person's shift: a night shift from 22:00 to 06:00 stays one day. For people without a shift, a work day starts at 04:00 (Settings → Advanced), so an 01:30 check-out joins the evening before.
- Phone in, door out. If someone checks in on their phone or the web and leaves through a machine, the machine punch closes that check-in.
- Edits are never overwritten. Once a person edits a clock-in that came from a machine, Tarleaks stops changing that day; new punches for it wait for review.
- Manual entries. A manual entry that overlaps a machine day stops that day too, rather than deleting anything.
- Locked weeks. Once a timesheet week is submitted or approved, machine punches for it wait for review — payroll only ever receives approved weeks.
- Old punches. Punches for days more than 35 days ago (0 to 400, Settings → Advanced) wait under the reason "too old". Someone with the import permission can release them from the machine's page.
- What a manager sees. Machine problems (missing check-out, missing check-in, very long session, replaced) appear in Attendance → Flagged clock-ins with the other flags.
Troubleshooting
| What you see | What to do |
|---|---|
| The machine page says Never connected | Check the address (no http://, no spaces), port 443 and HTTPS ON. Check the machine can reach the internet (gateway and DNS set in its network menu). Older firmware may not trust our Let's Encrypt certificate or may not speak TLS 1.2 — connect it through the bridge's ADMS relay. |
| 401 / "UNAUTHORIZED" | The key is wrong or was replaced. Copy the current key or issue a new one, and type it into the machine again. |
| 402 / "NOT LICENSED" | The workspace has no Attendance licence. |
403 "integrations off" / DEVICE_INTEGRATIONS_OFF | Settings → Features → Attendance → Clock-in machines is off. |
403 "ADDRESS NOT ALLOWED" / DEVICE_ADDRESS_NOT_ALLOWED | The request came from an address the machine does not allow. Allow it on the machine's page (Connection → Allowed addresses). |
| 403 "serial mismatch" | The serial number saved on the machine's page differs from the machine's own. Correct it on the page. |
| "machines are trying to connect" on the machines list | A machine is knocking with the serial-number address but has not been added. Add it (type its serial number) or dismiss it. |
| "This serial number is already registered in another place" | Use the key address for this machine instead (Connection tab), or contact support. |
| Punches arrive but no clock-ins | Machine users are not matched yet: Attendance → Machine users → Not matched. Also check Settings → Attendance → Allowed sources includes Clock-in machines. |
| Every punch is "in" | Switch the in/out rule to First and last punch of the day. |
| Times are off by hours | The machine's time zone differs from the one in Tarleaks. Set both to the same zone (machine page → Advanced). |
Punches refused as "clock ahead" (time_in_future) | The machine's clock is ahead. Use Set the machine's time on its page, or fix the clock on the machine. |
| "Duplicates" in a report | Normal: those punches were already stored. |
| Hikvision events show as "not a pass event" | The terminal sends a different event code. Add it under machine page → Advanced → accepted event codes. |
| 429 | Too many requests: wait Retry-After, send larger batches less often. |
| Old punches waiting | They are older than the back-fill window. Release them from the machine's page. |
What's next
Direct connections to the vendors' own clouds — ZKTeco BioTime, Suprema BioStar 2 / Air, Anviz CrossChex Cloud and Hikvision HikCentral — are not available yet. Until then, export their attendance reports and import them, or have an integrator send them through the punch API.
Appendix
ADMS as implemented
The machine's start-up exchange (GET /iclock/cdata?SN=…&options=all) is answered with:
GET OPTION FROM: <serial>
ATTLOGStamp=<last stamp>
OPERLOGStamp=<last stamp>
ATTPHOTOStamp=None
Stamp=<last stamp>
OpStamp=<last stamp>
ErrorDelay=60
Delay=30
TransTimes=00:00;14:05
TransInterval=1
TransFlag=TransData AttLog OpLog EnrollUser ChgUser
TimeZone=4
Realtime=1
Encrypt=None
ServerVer=2.4.1
TransFlag asks for punches and user records only. TimeZone is in hours, or in minutes when the zone is not a whole number of hours (330).
| Request | Handling |
|---|---|
POST cdata?table=ATTLOG | Lines PIN, time, status, verify, work code, …; only the first five fields are read. Answered OK only after the punches are stored. |
POST cdata?table=OPERLOG or USERINFO | Only USER lines, and only PIN, Name and Card. |
POST cdata?table=ATTPHOTO, FINGERTMP, FACE, BIODATA, USERPIC, BIOPHOTO, rtlog | Thrown away unread; answered OK. |
GET getrequest | Heartbeat; returns waiting commands as C:<id>:<command> lines. |
POST devicecmd | The machine's command results. |
GET rtdata?type=time | DateTime=<encoded UTC time>,ServerTZ=+0400. |
Commands sent to the machine: set the time SET OPTION DateTime=…, fetch punches again DATA QUERY ATTLOG StartTime=… EndTime=…, read the user list DATA QUERY USERINFO, restart REBOOT.
Status number → in/out: 0 in, 1 out, 2 break out, 3 break in, 4 overtime in, 5 overtime out, anything else unknown. Verify number → how verified: 1 fingerprint, 2 or 4 card, 0 or 3 PIN, 15 face, 25 palm, anything else other.
Hikvision event fields read
eventType (AccessControllerEvent; anything else is a heartbeat), dateTime, and from AccessControllerEvent: majorEventType, subEventType, employeeNoString, cardNo, name, attendanceStatus, serialNo, currentVerifyMode. Nothing else is stored — not pictureURL, not temperature, not mask.
Punch status and reason codes
| status | reasons |
|---|---|
received | — (waiting to be processed) |
unmatched | no_person_for_ref — nobody is matched to this machine user |
processed | — |
duplicate_read | double_read |
ignored | before_import_start (before the machine's start date), not_a_pass_event (a Hikvision event that is not a pass), ignored_by_admin (set aside by a person) |
needs_review | too_old, employee_not_active (not employed on that day), biometric_source_disabled (Clock-in machines not in Allowed sources), week_locked (timesheet submitted or approved), entry_edited (a person edited that day), overlaps_other_entry (a manual or web entry overlaps), correction_pending (a correction request on that day is waiting for a decision; retried automatically once it is decided) |
rejected | time_in_future (more than 5 minutes ahead of the server) |
Flags on clock-ins made from machine punches: missing_check_out, missing_check_in, long_session, superseded.