ZKTeco Push Protocol Explained: Why It Matters for Cloud Attendance
ZKTeco devices support two connection modes: pull (SDK) and push (cloud). Learn how push mode eliminates static IPs, local servers, and polling β and how to get real-time attendance data through a REST API.
Introduction
You bought a ZKTeco device. You connected it to the network. Now you need attendance data in your cloud application. The question is: does the device push data to you, or do you pull it from the device?
That one architectural decision β push vs. pull β determines whether your integration works reliably at scale or breaks the moment you add a second office. Most developers start with the pull approach (SDK libraries), hit a wall, and then discover push mode exists. By then, they've already burned weeks.
This article explains what push mode is, why it exists, how it compares to the SDK approach, and how to get real-time attendance data without touching raw device protocols.
Pull Mode: The SDK Approach (and Why It Breaks)
The traditional way to get data from a ZKTeco device is pull mode. Your server connects to the device over the local network, authenticates, and requests records. Libraries like pyzk, node-zklib, and zklib all work this way.
It feels simple at first. Write a script, connect to the device IP, call get_attendance(), done. But the simplicity is deceptive.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 340" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="25" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Pull Mode: Server Connects to Device</text>
<!-- Server -->
<rect x="40" y="70" width="180" height="80" rx="12" stroke="#f87171" stroke-width="2" fill="none"/>
<text x="130" y="105" text-anchor="middle" fill="#f87171" font-size="15" font-family="system-ui">π» Your Server</text>
<text x="130" y="127" text-anchor="middle" fill="#f87171" font-size="14" font-family="system-ui">(Must be on LAN)</text>
<!-- Arrow -->
<line x1="220" y1="110" x2="340" y2="110" stroke="#64748b" stroke-width="2" marker-end="url(#ppArrow)"/>
<text x="280" y="98" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">connect + poll</text>
<!-- Device -->
<rect x="340" y="70" width="180" height="80" rx="12" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="430" y="105" text-anchor="middle" fill="#22d3ee" font-size="15" font-family="system-ui">π ZKTeco Device</text>
<text x="430" y="127" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">(Static IP needed)</text>
<!-- Problems list -->
<rect x="40" y="190" width="680" height="130" rx="10" stroke="#f87171" stroke-width="1.5" fill="none" stroke-dasharray="6,4"/>
<text x="60" y="215" fill="#f87171" font-size="15" font-weight="bold" font-family="system-ui">β Pull Mode Limitations:</text>
<text x="60" y="240" fill="#94a3b8" font-size="14" font-family="system-ui">β’ Server must be on the same network as the device (no cloud apps)</text>
<text x="60" y="262" fill="#94a3b8" font-size="14" font-family="system-ui">β’ Device needs a static IP β DHCP changes break the connection</text>
<text x="60" y="284" fill="#94a3b8" font-size="14" font-family="system-ui">β’ Polling creates delays (1-5 min) and wasted bandwidth</text>
<text x="60" y="306" fill="#94a3b8" font-size="14" font-family="system-ui">β’ Each device = one connection. 50 devices = 50 threads polling continuously</text>
<defs>
<marker id="ppArrow" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#64748b"/></marker>
</defs>
</svg>
Here's what goes wrong in production:
- Your app runs in the cloud. AWS, Azure, Railway, Vercel β none of these can reach a device behind a router on a corporate LAN. You need a local server running 24/7 as a bridge.
- Devices get new IPs. IT resets the router, DHCP lease expires, someone unplugs the switch. Your connection string is now wrong, and you're not getting data until someone notices.
- Polling isn't real-time. You're asking the device "any new records?" every 30 seconds. That's bandwidth wasted on empty responses 95% of the time β and a 30-second delay when data does arrive.
- It doesn't scale. Managing persistent connections to 50+ devices across multiple offices, each with its own network configuration? That's a full-time ops job.
Pull mode works for a single device on a LAN. The moment you go multi-site or cloud-based, you need push mode.
Push Mode: The Device Connects to You
Push mode flips the connection direction. The device initiates the connection to a cloud server, not the other way around. The device doesn't need a static IP. Your server doesn't need to be on the same network. The device just needs internet access.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 320" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="25" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">Push Mode: Device Connects to Cloud</text>
<!-- Device 1 -->
<rect x="20" y="60" width="170" height="65" rx="10" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="105" y="88" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">π Office A Device</text>
<text x="105" y="108" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">(DHCP β any IP)</text>
<!-- Device 2 -->
<rect x="20" y="145" width="170" height="65" rx="10" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="105" y="173" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">π Office B Device</text>
<text x="105" y="193" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">(Different network)</text>
<!-- Device 3 -->
<rect x="20" y="230" width="170" height="65" rx="10" stroke="#22d3ee" stroke-width="2" fill="none"/>
<text x="105" y="258" text-anchor="middle" fill="#22d3ee" font-size="14" font-family="system-ui">π Office C Device</text>
<text x="105" y="278" text-anchor="middle" fill="#64748b" font-size="12" font-family="system-ui">(Behind NAT)</text>
<!-- Arrows to cloud -->
<line x1="190" y1="92" x2="310" y2="160" stroke="#34d399" stroke-width="2" marker-end="url(#ppArrow2)"/>
<line x1="190" y1="177" x2="310" y2="170" stroke="#34d399" stroke-width="2" marker-end="url(#ppArrow2)"/>
<line x1="190" y1="262" x2="310" y2="180" stroke="#34d399" stroke-width="2" marker-end="url(#ppArrow2)"/>
<text x="260" y="140" text-anchor="middle" fill="#34d399" font-size="13" font-family="system-ui">push</text>
<!-- Cloud -->
<rect x="310" y="130" width="190" height="80" rx="12" stroke="#a78bfa" stroke-width="2" fill="none"/>
<text x="405" y="165" text-anchor="middle" fill="#a78bfa" font-size="15" font-family="system-ui">βοΈ Cloud Protocol</text>
<text x="405" y="187" text-anchor="middle" fill="#a78bfa" font-size="15" font-family="system-ui">Engine</text>
<!-- Arrow to app -->
<line x1="500" y1="170" x2="580" y2="170" stroke="#64748b" stroke-width="2" marker-end="url(#ppArrow2)"/>
<text x="540" y="158" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">REST API</text>
<!-- Your app -->
<rect x="580" y="130" width="190" height="80" rx="12" stroke="#34d399" stroke-width="2" fill="none"/>
<text x="675" y="165" text-anchor="middle" fill="#34d399" font-size="15" font-family="system-ui">π» Your App</text>
<text x="675" y="187" text-anchor="middle" fill="#34d399" font-size="14" font-family="system-ui">(Anywhere)</text>
<defs>
<marker id="ppArrow2" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#34d399"/></marker>
</defs>
</svg>
In push mode, the device does the heavy lifting. It connects outbound to the cloud, sends attendance data as events happen, receives commands (sync time, enroll users, restart), and reports its status β all without your server ever needing to initiate a connection.
This is how ZKTeco devices are meant to work in cloud environments. Most modern ZKTeco devices (manufactured after 2015) support push mode out of the box. You configure the server address in the device settings, and it starts pushing.
Why Push Mode Wins
| Factor | Pull (SDK) | Push (Cloud) |
|--------|-----------|--------------|
| Network requirement | Same LAN | Internet only |
| Device IP | Static required | Any (DHCP fine) |
| Latency | 30sβ5min polling delay | Real-time (<1s) |
| Cloud apps | β Needs local bridge | β
Native |
| Multi-site | β VPN per site | β
All push to one endpoint |
| Scaling | 1 thread per device | Event-driven, no polling |
| Firewall | Inbound ports open | Outbound only (like a browser) |
The Hard Part: Building a Push Protocol Server
Here's where most teams get stuck. Push mode is better, but implementing the server that receives push data is complex. The device speaks a proprietary protocol. You need to:
- Parse the device's connection handshake correctly
- Handle authentication and device registration
- Process attendance records from the raw data format
- Queue and deliver commands back to the device
- Manage connection state for dozens of devices simultaneously
- Handle reconnections, timeouts, and data deduplication
- Store and replay records when your downstream system is offline
This is not a weekend project. Open-source attempts exist, but most handle only basic attendance fetching and break under load. Building a production-grade push protocol server took our team months of work and thousands of hours of testing with 80+ devices in the field.
You have two options:
1. Build it yourself β plan 3-6 months of protocol engineering
2. Use a middleware that already handles the protocol and gives you a clean API
The Middleware Approach: Protocol In, REST API Out
PunchConnect sits between your devices and your application. Devices push data to PunchConnect's protocol engine. You consume clean REST API endpoints and webhooks. You never touch the raw protocol.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 260" fill="none" style="width:100%;max-width:800px;">
<text x="400" y="25" text-anchor="middle" fill="#94a3b8" font-size="17" font-weight="bold" font-family="system-ui">PunchConnect: Protocol Abstraction Layer</text>
<!-- Raw protocol side -->
<rect x="20" y="60" width="200" height="170" rx="12" stroke="#f87171" stroke-width="1.5" fill="none" stroke-dasharray="6,4"/>
<text x="120" y="85" text-anchor="middle" fill="#f87171" font-size="14" font-weight="bold" font-family="system-ui">You Don't Touch This</text>
<text x="120" y="110" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Device handshake</text>
<text x="120" y="130" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Raw protocol parsing</text>
<text x="120" y="150" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Command queuing</text>
<text x="120" y="170" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Connection management</text>
<text x="120" y="190" text-anchor="middle" fill="#64748b" font-size="13" font-family="system-ui">Retry + deduplication</text>
<text x="120" y="215" text-anchor="middle" fill="#f87171" font-size="13" font-family="system-ui">β¬
PunchConnect handles</text>
<!-- Arrow -->
<line x1="220" y1="145" x2="290" y2="145" stroke="#a78bfa" stroke-width="2" marker-end="url(#ppArrow3)"/>
<!-- PunchConnect -->
<rect x="290" y="90" width="180" height="110" rx="12" stroke="#a78bfa" stroke-width="2" fill="none"/>
<text x="380" y="125" text-anchor="middle" fill="#a78bfa" font-size="16" font-weight="bold" font-family="system-ui">PunchConnect</text>
<text x="380" y="148" text-anchor="middle" fill="#a78bfa" font-size="14" font-family="system-ui">Protocol β REST</text>
<text x="380" y="170" text-anchor="middle" fill="#a78bfa" font-size="14" font-family="system-ui">Conversion Engine</text>
<!-- Arrow -->
<line x1="470" y1="145" x2="540" y2="145" stroke="#a78bfa" stroke-width="2" marker-end="url(#ppArrow3)"/>
<!-- Clean API side -->
<rect x="540" y="60" width="230" height="170" rx="12" stroke="#34d399" stroke-width="2" fill="none"/>
<text x="655" y="85" text-anchor="middle" fill="#34d399" font-size="14" font-weight="bold" font-family="system-ui">You Work With This</text>
<text x="655" y="110" text-anchor="middle" fill="#94a3b8" font-size="13" font-family="system-ui">GET /api/attendance</text>
<text x="655" y="130" text-anchor="middle" fill="#94a3b8" font-size="13" font-family="system-ui">GET /api/devices</text>
<text x="655" y="150" text-anchor="middle" fill="#94a3b8" font-size="13" font-family="system-ui">POST /api/employees</text>
<text x="655" y="170" text-anchor="middle" fill="#94a3b8" font-size="13" font-family="system-ui">Webhook events</text>
<text x="655" y="190" text-anchor="middle" fill="#94a3b8" font-size="13" font-family="system-ui">JSON everywhere</text>
<text x="655" y="215" text-anchor="middle" fill="#34d399" font-size="13" font-family="system-ui">β¬
Your code uses this</text>
<defs>
<marker id="ppArrow3" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#a78bfa"/></marker>
</defs>
</svg>
Getting Real-Time Attendance Data (Code Examples)
Once your device is configured in the PunchConnect dashboard (takes about 5 minutes), you interact exclusively with the REST API. Here's how to get attendance data:
Fetch Recent Attendance Records
Response:
# Get today's attendance eventscurl -X GET "https://api.punchconnect.com/v1/attendance?from=2026-03-27T00:00:00Z" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json"
Python β Fetch and Process Attendance
import requestsfrom datetime import datetime, timezoneAPI_KEY = "YOUR_API_KEY"BASE_URL = "https://api.punchconnect.com/v1"def get_attendance(from_date: str = None):"""Fetch attendance records from PunchConnect."""params = {}if from_date:params["from"] = from_dateresponse = requests.get(f"{BASE_URL}/attendance",headers={"Authorization": f"Bearer {API_KEY}"},params=params,)response.raise_for_status()return response.json()["data"]def sync_to_your_system(records):"""Push attendance records to your HR system."""for record in records:print(f"[{record['punch_time']}] "f"{record['employee_id']} β "f"{record['punch_type']} "f"({record['verify_mode']})")# your_hr_system.create_attendance(# employee=record["employee_id"],# timestamp=record["punch_time"],# type=record["punch_type"],# )if __name__ == "__main__":today = datetime.now(timezone.utc).strftime("%Y-%m-%dT00:00:00Z")records = get_attendance(from_date=today)print(f"Fetched {len(records)} records")sync_to_your_system(records)
JavaScript β Real-Time Webhook Listener
Instead of polling the API, you can receive instant webhook notifications the moment a punch happens:
import express from "express";const app = express();app.use(express.json());app.post("/webhook/attendance", (req, res) => {const event = req.body;// Verify the webhook signature (recommended)// verifySignature(req.headers["x-punchconnect-signature"], req.body);console.log(`New punch: ${event.employee_id} at ${event.punch_time}`);console.log(` Type: ${event.punch_type} | Method: ${event.verify_mode}`);console.log(` Device: ${event.device_serial}`);// Process in your system// await hrSystem.recordAttendance(event);res.status(200).json({ received: true });});app.listen(3000, () => {console.log("Webhook listener running on port 3000");});
Check Device Status
# See all devices and their connection statuscurl -X GET "https://api.punchconnect.com/v1/devices" \-H "Authorization: Bearer YOUR_API_KEY"
Which ZKTeco Devices Support Push Mode?
Most modern ZKTeco devices support push mode. Any device running ZMM firmware (the majority of devices manufactured after 2015) has push capability. This includes:
- Face recognition: SpeedFace V5L, SpeedFace-H5, ProFace X
- Fingerprint: UA860, K40, IN01-A, X7
- Multi-modal: SpeedFace V4L, MultiBio 800
- Access control: inBio 160/260, C3-100/200/400
How to check: Look for the "Cloud Server" or "ADMS Server" setting in your device's network configuration menu. If it's there, your device supports push mode.
Configuring push mode in PunchConnect: You register your device in the dashboard, get a server address, enter it in the device settings, and the device starts pushing. The entire process takes about 5 minutes per device. No software installation, no SDK, no local server.
Push Mode vs. Pulling with SDK Libraries: A Real Comparison
We've deployed both approaches in production. Here's what we've seen:
A logistics company with 12 offices started with pyzk running on a Raspberry Pi at each site. Maintenance cost: one full-time engineer managing 12 Pis, troubleshooting network changes, restarting crashed scripts, and dealing with data gaps when Pis went offline. After switching to push mode with PunchConnect: zero on-site infrastructure, 99.5% data delivery rate, and the engineer was reassigned to product work.
An agriculture company (AgriWise) with 24,000+ employees across 50+ sites needed real-time attendance in their ERP. Pull mode was never an option β you can't maintain persistent connections to 80+ devices across a country. Push mode with PunchConnect handles all of it from a single cloud endpoint. Read the full case study β
The pattern is clear: pull mode works for prototypes, push mode works for production.
Common Questions About Push Mode
Do I need to open firewall ports for push mode?
No. Push mode uses outbound connections from the device, just like a web browser. If your device can access the internet, it can push data. No inbound ports, no static IP, no port forwarding needed.
How real-time is push mode?
Sub-second. When an employee punches on the device, the event reaches PunchConnect and triggers your webhook within 1 second. Compared to SDK polling (30 seconds to 5 minutes), push mode is effectively instant.
What happens if the internet goes down?
The device buffers locally. ZKTeco devices store records in onboard memory (typically 50,000β100,000 records). When connectivity returns, the device automatically pushes all buffered records. PunchConnect deduplicates them, so you never get duplicate entries.
Can I send commands back to the device in push mode?
Yes. PunchConnect queues commands (enroll employees, sync time, restart, update settings) and delivers them to the device on the next push cycle. You send commands via the REST API, and PunchConnect handles delivery.
# Enroll an employee on a devicecurl -X POST "https://api.punchconnect.com/v1/devices/BFEZ203901245/commands" \-H "Authorization: Bearer YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"command": "enroll_employee","params": {"employee_id": "emp_099","name": "Sarah Johnson","card_number": "0012345678"}}'
Is push mode compatible with all ZKTeco models?
Push mode works with any ZKTeco device that has the "Cloud Server" or "ADMS Server" setting β which is the vast majority of devices made after 2015. Older devices (pre-2015) may only support pull mode, but these are increasingly rare in active deployments.
Getting Started
If you're building a biometric integration and trying to decide between push and pull:
1. Start with push mode. It's the right architecture for any cloud-based application.
2. Don't build the protocol server yourself β unless you have months to spare and deep experience with ZKTeco device protocols.
3. Try PunchConnect free for 7 days β register your device, get your API key, and receive your first attendance webhook in under 15 minutes. No credit card required.
If you're already using an SDK library and hitting scaling issues, read our comparison: Why Open-Source ZKTeco Libraries Won't Scale. And if your biggest blocker is the static IP requirement, see Biometric Attendance Without a Static IP.
---
*PunchConnect is production-proven middleware connecting ZKTeco biometric devices to any software system. Currently powering attendance for 24,000+ employees across 50+ sites.*