ERPNext 7 min read Updated
Connecting biometric attendance devices to ERPNext
Sync ZKTeco-style biometric devices with ERPNext: Employee Checkin, Shift Type auto attendance, Frappe's sync tool, timezones, duplicates and offline devices.
Written by the erpfly team, people who build on Frappe and Odoo for a living.
To connect a biometric attendance device to ERPNext, you get each punch into an Employee Checkin record, then let a Shift Type with auto attendance turn those check-ins into Attendance. For ZKTeco and compatible devices on your network, Frappe’s open-source biometric-attendance-sync-tool does the first part. It pulls punch logs from the device over the local network using the pyzk library and posts them to ERPNext’s API with an API key and secret. Most of the effort goes into the details around it: matching device user IDs to employees, timezones, duplicate punches and devices that go offline.
Here’s how the pieces fit, and where we’ve seen setups go wrong.
The two DocTypes that matter
Employee Checkin
Every punch becomes one Employee Checkin. The fields you care about are employee, time, log_type (IN, OUT or blank), device_id and skip_auto_attendance. When a check-in is saved, HRMS works out which shift it belongs to and fills in the shift fields for you.
ERPNext won’t accept two check-ins for the same employee with exactly the same timestamp. That rule saves you from full duplicate imports, but as you’ll see below, it doesn’t help with the more common kind of duplicate.
To map device users to employees, set Attendance Device ID (Biometric/RF tag ID) on each Employee record (fieldname attendance_device_id) to the user ID enrolled on the device. If that field is blank or wrong, punches for that person are rejected. Get this right for everyone before you switch anything on.
Shift Type with auto attendance
On the Shift Type, tick Enable Auto Attendance and look at these settings carefully:
- Determine Check-in and Check-out. Either “Alternating entries as IN and OUT during the same shift” or “Strictly based on Log Type in Employee Checkin”. Use the second only if your devices reliably record direction.
- Working Hours Calculation Based On. “First Check-in and Last Check-out” is forgiving. “Every Valid Check-in and Check-out” is stricter and punishes missed punches.
- Begin check-in before shift start time and Allow check-out after shift end time, which define the window a punch counts toward.
- Working hours thresholds for half day and absent, and the late entry and early exit grace periods.
- Process Attendance After, so auto attendance doesn’t try to rebuild history from before you went live.
- Last Sync of Checkin.
That last field is the one people skip. Auto attendance only processes a shift once it knows all the check-ins for that shift have arrived, and it uses Last Sync of Checkin to decide that. If nothing updates it, attendance never gets marked, or gets marked from incomplete data. The sync tool updates it for you if you configure the mapping, which we’ll get to.
Auto attendance runs on the scheduler, and it can also mark employees absent when they’re assigned to the shift and have no check-ins at all. Make sure Shift Assignments or default shifts on Employee records are correct, or you’ll get a lot of false absences on day one.
Setting up the sync tool
The tool lives at github.com/frappe/biometric-attendance-sync-tool. It’s a Python script meant to run continuously on a machine that can reach the devices on the local network, plus a simple GUI variant for people who’d rather not edit a config file.
Create an API user
Create a dedicated user in ERPNext for the integration, generate an API key and secret from that user’s settings, and give it only the role permissions it needs to create Employee Checkin records. Don’t use Administrator’s keys. The keys will sit in a plain text file on a box in a server cupboard, so treat them accordingly.
You can check the keys work before touching the device:
curl -X POST \
"https://erp.example.com/api/method/hrms.hr.doctype.employee_checkin.employee_checkin.add_log_based_on_employee_field" \
-H "Authorization: token API_KEY:API_SECRET" \
-H "Content-Type: application/json" \
-d '{"employee_field_value": "1042", "timestamp": "2026-08-03 08:57:13", "device_id": "gate1", "log_type": "IN"}'
That’s the whitelisted method the tool calls, on v14 and later where HR lives in the separate HRMS app. It looks up the employee by attendance_device_id and creates the check-in. Delete the test record afterwards.
Configure local_config.py
Copy local_config.py.template to local_config.py and fill it in. A trimmed example:
ERPNEXT_API_KEY = "your_api_key"
ERPNEXT_API_SECRET = "your_api_secret"
ERPNEXT_URL = "https://erp.example.com"
ERPNEXT_VERSION = 15
PULL_FREQUENCY = 60 # minutes between pulls
LOGS_DIRECTORY = "logs"
IMPORT_START_DATE = "20260801" # ignore punches before go-live
devices = [
{"device_id": "gate1", "ip": "192.168.1.201",
"punch_direction": None, "clear_from_device_on_fetch": False},
{"device_id": "warehouse1", "ip": "192.168.1.202",
"punch_direction": None, "clear_from_device_on_fetch": False},
]
shift_type_device_mapping = [
{"shift_type_name": ["General Shift"], "related_device_id": ["gate1", "warehouse1"]},
]
A few notes from real installs:
ERPNEXT_VERSIONtells the tool which API path to use. Set it to your actual major version.device_idmust be unique and alphanumeric. It’s stored on every Employee Checkin, so pick names that mean something to HR when they’re investigating a dispute.punch_directioncan be"IN"or"OUT"for a device that only handles one direction,"AUTO"to use the in/out state the device records, orNoneto leave log type blank and let the Shift Type alternate.shift_type_device_mappingis what updates Last Sync of Checkin. List every shift type served by those devices.allowed_exceptionsin the template controls which errors are skipped rather than halting the import: employee not found, inactive employee, and duplicate check-in. The defaults are sensible.
Run it under systemd, supervisor or a Windows service so it restarts after power cuts. The tool keeps its state and error logs in LOGS_DIRECTORY, which is the first place to look when punches stop arriving.
What the tool does under the hood
If you’re debugging or writing your own integration, the pyzk calls are simple:
from zk import ZK
zk = ZK("192.168.1.201", port=4370, timeout=10)
conn = zk.connect()
try:
conn.disable_device() # stop the device changing mid-read
print("device clock:", conn.get_time())
for att in conn.get_attendance():
print(att.user_id, att.timestamp, att.punch)
finally:
conn.enable_device()
conn.disconnect()
user_id is what must match attendance_device_id on the Employee. timestamp is a naive datetime from the device’s own clock, which leads straight to the most common problem.
Timezones and clock drift
Frappe stores datetimes without a timezone, interpreted in the timezone set in System Settings. The device also records local time without a timezone. So the device clock has to be set to the same timezone as your ERPNext site. Not the timezone of the server it’s hosted on, and not UTC because the cloud box runs in UTC.
When these disagree, everything looks almost right. Punches are all shifted by a fixed number of hours, night shift check-ins land on the wrong date, and auto attendance marks people late or absent in patterns that make no sense until you spot the offset.
Device clocks also drift, and some reset after a power loss. Compare conn.get_time() against real time as part of routine checks, and correct it with conn.set_time() or from the device menu. If you have a site in another timezone, its device must still record in the site timezone set in ERPNext, or you need custom handling.
Duplicates
Exact duplicates (same employee, same second) are rejected by ERPNext and skipped by the tool. The ones that hurt are near-duplicates: someone presses a finger twice because the first beep was quiet, producing two punches a few seconds apart.
With “Alternating entries as IN and OUT”, that double tap flips everything after it. Their morning IN becomes IN then OUT, lunch becomes IN, and the end of day becomes an unmatched IN. Use “First Check-in and Last Check-out” for working hours where your policy allows it, since it’s far less sensitive to extra punches in the middle. If you need strict IN/OUT tracking, use devices with direction keys or separate entry and exit readers, and configure punch_direction accordingly.
Don’t clear the tool’s status file in the logs directory to “force a resync” unless you understand it will re-send everything since IMPORT_START_DATE. The duplicate check will reject most of it, but your error log will be unreadable for a day.
Offline devices
Devices go offline. Network switches get unplugged, DHCP hands out a new IP, a branch loses internet. Pull-based sync handles this reasonably well: punches stay stored on the device, and the tool picks them up on the next successful pull.
Three things to get right:
- Give devices static IPs or DHCP reservations. A device that moves address just looks offline forever.
- Keep
clear_from_device_on_fetchoff. Clearing logs from the device after each fetch means a failed post can lose punches with no copy anywhere. Device storage is finite, so plan to clear logs manually on a schedule after checking ERPNext has them. - Alert on silence. If a device hasn’t produced a check-in during working hours, someone should hear about it that morning, not at payroll.
Many newer devices also support a push mode, where the device sends punches to a server URL instead of waiting to be polled. The Frappe sync tool doesn’t use that protocol, so push-mode setups need a different receiver.
Our opinion: keep the sync tool off the ERP server
We prefer running the sync tool on a small, always-on machine at the site with the devices, not on the ERPNext server. The devices speak a local protocol on port 4370 and have no business being exposed to the internet. The on-site box only needs outbound HTTPS to ERPNext. For multiple branches, one small box per site, each with its own device IDs, beats a VPN mesh back to head office.
And resist the urge to fix attendance by editing Employee Checkin records in bulk. Fix the cause (mapping, clock, shift settings), then re-run attendance for the affected period.
When the standard setup isn’t enough
Some teams need more than this: approval flows for missed punches, overtime rules that don’t fit Shift Type, or devices that only speak a push protocol. That’s where a small Frappe app beats a pile of Server Scripts, as we argue in Server Scripts vs a custom app. erpfly’s attendance management module is a starting point for that kind of build, and it plugs into HR and payroll and leave management. For anything else around HRMS, see our ERPNext customization service.
Terms in this post