# Connecting biometric attendance devices to ERPNext

Canonical: https://erpfly.com/blog/erpnext-biometric-attendance-integration/
Last updated: September 15, 2026

Published August 18, 2026 in ERPNext. Sync ZKTeco-style biometric devices with ERPNext: Employee Checkin, Shift Type auto attendance, Frappe's sync tool, timezones, duplicates and offline devices.

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 and the same log type (two IN punches at the same second, say). 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:

```bash
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:

```python
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_VERSION`** tells the tool which API path to use. Set it to your actual major version.
- **`device_id`** must 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_direction`** can be `"IN"` or `"OUT"` for a device that only handles one direction, `"AUTO"` to use the in/out state the device records, or `None` to leave log type blank and let the Shift Type alternate.
- **`shift_type_device_mapping`** is what updates Last Sync of Checkin. List every shift type served by those devices.
- **`allowed_exceptions`** in 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:

```python
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_fetch` off.** The tool does write fetched punches to a dump file in its logs folder before clearing the device, and retries from it on the next run, but that file sits on one machine and its own config comment warns that clearing can lose data if used carelessly. 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](https://erpfly.com/blog/server-scripts-vs-custom-app-erpnext/). erpfly's [attendance management module](https://erpfly.com/modules/attendance-management/) is a starting point for that kind of build, and it plugs into [HR and payroll](https://erpfly.com/modules/hr-payroll/) and [leave management](https://erpfly.com/modules/leave-management/). For anything else around HRMS, see our [ERPNext customization](https://erpfly.com/erpnext-customization/) service.

### Sources

- [Biometric attendance devices, Frappe HR documentation](https://docs.frappe.io/hr/integrating-frappe-hr-with-biometric-attendance-devices)
- [Shift Type, Frappe HR documentation](https://docs.frappe.io/hr/shift-type)
- [Employee Checkin, Frappe HR documentation](https://docs.frappe.io/hr/employee-checkin)
- [local_config.py.template, biometric-attendance-sync-tool](https://github.com/frappe/biometric-attendance-sync-tool/blob/master/local_config.py.template)
- [erpnext_sync.py, biometric-attendance-sync-tool](https://github.com/frappe/biometric-attendance-sync-tool/blob/master/erpnext_sync.py)