العربية

Start free trialBook a demo

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 machineWhat to doSection
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 folderInstall the small bridge program on a PC at the site.The on-site bridge
Any other brandImport 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'sCall the punch API.The punch API reference
A machine that can only save its log to a USB stickImport 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:

DataKept for
Punches that became clock-ins, double reads and punches waiting for review24 months by default (Settings → Attendance → Clock-in machines → Advanced: 6 to 120 months)
Punches nobody was matched to, punches set aside, punches refused90 days
The sender's address and the raw machine line kept with each punchremoved after 90 days
Machines that tried to connect but were never added30 days
Finished machine commands90 days
Machine users never matched and not seen for the punch perioddeleted

The clock-ins themselves are ordinary time records and follow your normal attendance retention.

Before you start

  1. 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_URL in its .env), and the machine's Connection tab shows it with the right port. If that address is plain http:// on your office network, the steps say so and the machine's HTTPS switch stays off.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

  1. Add the machine in Tarleaks: Attendance → Clock-in machines → Add a machine → ZKTeco, eSSL or similar. Copy the connection key when it is shown.
  2. On the machine, set an administrator password (Menu → User Mgt → make one user Administrator), so nobody can read the connection key off its menu.
  3. On the machine: Menu → COMM → Cloud Server Setting (on some models: Comm. → ADMS).
  4. Turn Enable Domain Name ON.
  5. 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.
  6. Server Port: 443.
  7. HTTPS: ON. Proxy: OFF.
  8. Menu → System → Date/Time: set the time zone shown on the machine's Connection tab (for the UAE: UTC+04:00).
  9. 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").

  1. Add the machine in Tarleaks: Attendance → Clock-in machines → Add a machine → Hikvision terminal. Copy the connection key.
  2. Set a strong administrator password on the terminal's web page.
  3. On the terminal's web page: Configuration → Network → Advanced → HTTP Listening (on some models: Alarm Server).
  4. Protocol: HTTPS. Address type: domain name. Host: <company>.tarleaks.com. Port: 443.
  5. URL: /api/attendance/public/v1/hikvision/<key>/events — put the machine's connection key where <key> is.
  6. Format: JSON. Authentication: none (the key in the address is the credential).
  7. Event → Access Control → linkage: tick Upload to center (or Notify surveillance center) for authentication events.
  8. System → Time: NTP server pool.ntp.org, time zone GMT+04:00 (or your zone).
  9. 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 pathRequest bodyAnswer (200)
GET /device—the machine, its time zone and in/out rule, the workspace name, the server time and your limits
POST /punchesa batch of 1 to 500 punches (at most 512 KB)a report: accepted, duplicates, a result per punch, refused punches
POST /device-users1 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"
    }
  ]
}
FieldRequiredMeaning
personyes, 1–64 charactersWho punched: the machine's own user number by default.
timeyesYYYY-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_kindnodevice_user (default), employee_number (the person's employee number in Tarleaks) or badge (their badge or card number).
directionnoin, out, break_out, break_in, overtime_in, overtime_out or unknown.
status_codenoThe machine's raw state number, kept for reference.
verifynofingerprint, face, card, pin, palm or other.
work_codeno, up to 32The machine's work code, if used.
cardno, up to 64The card number read, if any.
idno, 1–160Your 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:

statusMeaning
receivedStored; it becomes a clock-in within a few minutes.
processedAlready part of a clock-in.
unmatchedStored, but nobody is matched to this machine user yet (Attendance → Machine users).
needs_reviewStored, but waiting for a person (see the reason codes in the appendix).
ignoredStored and set aside (for example, from before the machine's start date).
duplicate_readThe same person punched again within the double-read window; kept, not counted.
duplicateYou sent this punch before; nothing changed.
rejectedStored 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": "…"}}.

HTTPcodeWhat it means — what to do
400BAD_BATCHNot 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.)
401DEVICE_TOKEN_INVALIDThe key is missing, wrong, replaced or the machine was archived. Copy the current key, or issue a new one on the machine's page.
402ATTENDANCE_NOT_LICENSEDThe workspace has no Attendance licence. Ask your Tarleaks contact.
403WORKSPACE_SUSPENDEDThe workspace is suspended.
403DEVICE_INTEGRATIONS_OFFSettings → Features → Attendance → Clock-in machines is off (or switched off by the platform). Turn it on.
403DEVICE_INACTIVEThe machine is inactive or archived.
403DEVICE_ADDRESS_NOT_ALLOWEDThe machine has allowed addresses and this request came from another one. Allow it on the machine's page.
403WRONG_WORKSPACE_ADDRESSThe key belongs to another workspace than the address you called. Use your own workspace address.
408TRY_AGAINThe 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).
413BODY_TOO_LARGESend smaller batches.
429SLOW_DOWN or DAILY_LIMITToo many requests or punches. Wait the number of seconds in Retry-After, then send the same batch again.
503TRY_AGAINNothing 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:

kindpayloadWhat 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.zip
Windows: Get-FileHash .\tarleaks-attendance-bridge-0.1.0.zip -Algorithm SHA256
Linux / macOS: sha256sum tarleaks-attendance-bridge-0.1.0.zip

Reading 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):

adapterForSettings
zk_tcpZKTeco, eSSL, FingerTec on TCP port 4370address, 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_isapiHikvision terminals that cannot reach the internetaddress, username, password_env or password_file, poll_seconds (60)
adms_relayOlder ZKTeco firmware that can only push over plain httplisten (this PC's own LAN address and a port, never 0.0.0.0), serial (the one machine accepted), machine_ips (its address)
folderUSB exports (attlog.dat, .txt) or CSV files dropped in a folder — from any brand whose software saves its logpath, 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:

  1. 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.
  2. 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.
  3. 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):

RuleHow it worksExample: 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 buttonUses the in/out the person chose on the machine.two sessions 07:58–13:02 and 13:40–17:05 (if pressed correctly)
AlternateIn, 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 seeWhat to do
The machine page says Never connectedCheck 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_OFFSettings → Features → Attendance → Clock-in machines is off.
403 "ADDRESS NOT ALLOWED" / DEVICE_ADDRESS_NOT_ALLOWEDThe 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 listA 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-insMachine 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 hoursThe 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 reportNormal: 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.
429Too many requests: wait Retry-After, send larger batches less often.
Old punches waitingThey 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).

RequestHandling
POST cdata?table=ATTLOGLines 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 USERINFOOnly USER lines, and only PIN, Name and Card.
POST cdata?table=ATTPHOTO, FINGERTMP, FACE, BIODATA, USERPIC, BIOPHOTO, rtlogThrown away unread; answered OK.
GET getrequestHeartbeat; returns waiting commands as C:<id>:<command> lines.
POST devicecmdThe machine's command results.
GET rtdata?type=timeDateTime=<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

statusreasons
received— (waiting to be processed)
unmatchedno_person_for_ref — nobody is matched to this machine user
processed—
duplicate_readdouble_read
ignoredbefore_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_reviewtoo_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)
rejectedtime_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.