hafizasifali / zkteco-php
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.
Requires
- php: ^8.2
- ext-iconv: *
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- mockery/mockery: ^1.6
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
- illuminate/support: Enables the Laravel integration (Laravel 11, 12 or 13): config, facade, Artisan commands, events, storage and the push server routes.
Provides
None
Conflicts
- laravel/framework: <11.0
Replaces
None
This package is auto-updated.
Last update: 2026-09-15 07:22:43 UTC
README
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
iconvextension - 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:
- Timezones are explicit. Set
timezoneto 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. session()does not disable the device by default. A disabled device does not record punches, so disabling it during a read loses attendance. PassdisableDuring: truefor 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_approveoff 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.