Search by

hafizasifali / zkteco-php

hafizasifali

Modern PHP SDK for ZKTeco biometric attendance devices — fetch attendance, manage users and stream live punches over TCP/UDP, plus an ADMS push server. First-class Laravel support.

dev-main 2026-09-15 07:06 UTC

This package is auto-updated.

Last update: 2026-09-15 07:22:43 UTC


README

Tests Latest Version PHP Version License

A PHP library for ZKTeco biometric attendance and access-control devices: fingerprint, face and card terminals. Use it to fetch attendance, manage enrolled users and receive punches as they happen, in plain PHP or Laravel.

use HafizAsifAli\ZkTeco\ZkDevice;

$device = ZkDevice::make('192.168.1.201', timezone: 'Asia/Karachi');

$punches = $device->session(fn (ZkDevice $d) => $d->attendance()->since('-7 days'));

foreach ($punches as $punch) {
    echo "{$punch->userId} {$punch->direction->label()} at {$punch->timestamp->format('Y-m-d H:i')}\n";
}

Features

  • Two ways to connect
    • Direct connection: your server connects to the device on port 4370, over TCP or UDP. Many older panels only answer on UDP, and this library supports both.
    • Push server (ADMS / iclock): devices make outbound HTTP requests to your server. This works for devices behind NAT at branch offices, with no inbound firewall rules.
  • Attendance: download the log, filter by date, employee or direction, remove duplicates, group, and page through large logs.
  • Users: list, create, update and delete users; the next free slot is found automatically.
  • Live punches: a stream that delivers each punch within a second.
  • Device management: device info, capacity, clock drift and clock sync, enable/disable, restart, and fingerprint template copy between devices.
  • Laravel: config file, facade, 7 Artisan commands, events, an Eloquent model that stores each punch once, a queued sync job and push-server routes.
  • Correct timestamps: decoded times carry the device's own timezone, so they don't shift to the server's.
  • Non-Latin names: Arabic, Urdu, Chinese and other names survive the round trip through the device's own character encoding.
  • Tested: 111 tests, including real TCP and UDP sockets against a simulated device, and read operations verified on real hardware. PHPStan passes at level 6.

Requirements

  • PHP 8.2 or newer, with the iconv extension
  • Laravel 11, 12 or 13, only for the Laravel integration
  • Direct connection: network access to the device on port 4370
  • Push mode: devices that support ADMS / "Cloud Server"

Installation

composer require hafizasifali/zkteco-php

For Laravel, publish the config and migrations:

php artisan vendor:publish --tag=zkteco-config
php artisan vendor:publish --tag=zkteco-migrations
php artisan migrate

Quick start

Plain PHP

use HafizAsifAli\ZkTeco\ZkDevice;

$device = ZkDevice::make(
    host: '192.168.1.201',
    commKey: 0,              // the COMM key set on the device, 0 if none
    transport: 'tcp',        // use 'udp' for older panels
    timezone: 'Asia/Karachi' // the timezone the device clock is set to
);

// session() connects, runs your code, and always disconnects, even if your code throws.
$device->session(function (ZkDevice $device) {
    $info = $device->info()->snapshot();
    echo "Serial: {$info->serialNumber}, records: {$info->capacity->records}\n";

    $today = $device->attendance()->today();
    $users = $device->users()->all();
});

Laravel

ZKTECO_HOST=192.168.1.201
ZKTECO_TIMEZONE=Asia/Karachi
php artisan zkteco:test          # check the connection and show device details
php artisan zkteco:attendance    # show punches on the device
php artisan zkteco:sync          # download and store, skipping punches already stored
use HafizAsifAli\ZkTeco\Laravel\Facades\ZkTeco;
use HafizAsifAli\ZkTeco\Laravel\Jobs\SyncAttendance;
use HafizAsifAli\ZkTeco\Laravel\Models\Attendance;

// routes/console.php: sync every five minutes
Schedule::job(new SyncAttendance('main'))->everyFiveMinutes();

// Query stored punches
Attendance::forUser('1001')->onDate(today())->orderBy('punched_at')->get();

// Or talk to a device directly
$late = ZkTeco::device('main')->session(
    fn ($d) => $d->attendance()->today()->checkIns()->filter(
        fn ($punch) => $punch->time() > '09:15:00'
    )
);

Documentation

Guide What it covers
Getting started Device setup, finding the COMM key, TCP or UDP, first connection
Direct connection Complete API: attendance, users, device info, control, templates, live punches
Laravel Configuration, facade, Artisan commands, events, storage, jobs, multiple devices
Push server (ADMS) Receiving data from devices over HTTP, approving devices, sending commands
Troubleshooting Timeouts, wrong times, garbled names, missing punches
How the protocol works Packet format, authentication, bulk transfers, and known protocol limitations

Tested hardware

Device Firmware Platform Verified
ZKTeco KF160/ID (face + card) Ver 6.60 May 14 2018 ZMM220_TFT TCP and UDP: connection, device info, capacity, clock, users, downloading a log of 8,700+ records

Writes (users, templates, clock, clearing) are tested against the protocol simulator but haven't been run on this device. If the library works on your model, please add it here.

Choosing a connection mode

Direct connection Push server (ADMS)
Who connects Your server connects to the device The device connects to your server
Network Server must reach the device on port 4370 Device must reach your server over HTTP
Latency On each poll, or instant with listen() Near real time
Best for Devices on the same LAN or a VPN Branches behind NAT, many sites
Supported firmware Almost every ZKTeco terminal Models with ADMS or Cloud Server support

Both modes produce the same Punch objects, so your storage and reporting code works with either.

Upgrading from other ZKTeco libraries

Two behaviours may differ from older PHP ZKTeco libraries:

  1. Timezones are explicit. Set timezone to the device's timezone. Otherwise decoded times use PHP's default timezone, which is only correct if the server and device use the same one.
  2. session() does not disable the device by default. A disabled device does not record punches, so disabling it during a read loses attendance. Pass disableDuring: true for bulk writes.

Security

  • A COMM key is not real protection. The protocol's authentication scramble is weak and easily reversed (details in docs/protocol.md). Keep devices on an isolated network and never expose port 4370 to the internet.
  • The push server keeps new devices in a pending state until you approve them. Leave auto_approve off for any endpoint reachable from outside your network.
  • Values in push commands, such as names from your HR system, are sanitised, so a crafted name cannot add fields or commands.

To report a vulnerability, see SECURITY.md.

Testing

composer test      # PHPUnit
composer analyse   # PHPStan
composer lint      # Pint
composer check     # all three

Contributing

Contributions are welcome, especially reports from real hardware. Firmware varies a lot between models. If something decodes wrongly on your device, open an issue with the model, firmware version and, if you can, a hex dump of the data. See CONTRIBUTING.md.

Acknowledgements

The ZKTeco socket protocol is not officially documented. This library relies on the community's reverse-engineering of it, especially pyzk, the long-standing Python implementation, and was informed by msaied/zkteco-php. This package is an independent implementation, with its own design and its own fixes for several decoding and security problems described in the changelog.

ZKTeco is a trademark of ZKTeco Co., Ltd. This project is not affiliated with or endorsed by ZKTeco.

License

MIT. See LICENSE.