← All guides
Contents & other guides

Time & attendance

BioTime Integration Guide — HumanR

How to connect HumanR to a site that already runs ZKTeco BioTime as its device server, so the punches BioTime collects reach HumanR's attendance without touching a single terminal. Written for the IT head who owns the BioTime server and the network between it and HumanR; the HR steps (mapping PINs, enabling terminals) are here too. For terminals that push straight to HumanR, read the ZKTeco integration guide instead.

This is the guide HumanR users read inside the product, published as-is. It is written for someone with the screen in front of them, so it describes buttons you cannot click from here — which is rather the point: you can check how the product behaves before you commit to it.

1. How it fits together

  • BioTime stays the device server. A ZK terminal can push to only one host. Where that host is already BioTime — with enrolment, device settings, shift rules and the terminal list living there — HumanR does not ask for the terminals to be repointed. It logs in to BioTime's REST API as an ordinary API user and reads the transactions (punches) BioTime has already stored.
  • Pull, not push. HumanR calls BioTime; BioTime never calls HumanR. Nothing inbound needs opening on the HumanR side, and BioTime needs no plugin, webhook or database access.
  • Same landing path as a direct push. A pulled punch becomes the same punch row a pushing terminal would create: same (terminal, PIN, time) duplicate key, same terminal whitelist (new terminals arrive disabled), same Unmapped PINs page, same rollup into attendance days, same overtime and late-fine arithmetic. HR sees no difference downstream.
  • Both paths at once is fine. One site can push straight to /iclock/cdata while another sits behind BioTime. Several BioTime servers are supported too — each is a source with its own code, credentials, schedule and cursor.
  • It is a module. Behind the EnableBioTimeSync flag. Off is nothing: no page, no worker, no stored credentials used, no outbound call. The direct push path is unaffected either way.

2. What IT needs to have ready

ItemDetail
BioTime root URL, as seen from the HumanR server The address you log in to in a browser, with no path: http://biotime.example.local:8080. Absolute, http or https, no query string. If BioTime sits behind a reverse proxy that redirects http → https, give HumanR the final URL — it does not follow redirects, by design (§12).
Network path Outbound from the HumanR server to the BioTime host and port (BioTime's default web port is 8080; 80/443 behind a proxy). Nothing inbound to HumanR. If the two are on different segments or sites, open that one rule. Devices are untouched.
BioTime version 8.x or 9.x. 8.x issues API tokens at /api-token-auth/; 9.x at /jwt-api-token-auth/. HumanR tries the first and falls back to the second, so you do not have to tell it — but knowing which you run shortens any troubleshooting.
API module on the BioTime licence BioTime answers 403 to every API call when the API module is not licensed — on a trial or base licence, for instance. HumanR reports this as API not licensed. Confirm with your ZKTeco reseller before the go-live date; nothing in HumanR can work around it.
A dedicated, read-only BioTime user Made for HumanR (§3). Not a person's account: when they leave or reset their password the pull stops.
Clock and time zone BioTime's server clock in the same zone as HumanR's configured time zone, and the terminals in step with BioTime. Punch times are copied as the wall clock BioTime reports, with no conversion — a zone mismatch would shift every punch by hours. Test connection shows BioTime's newest punch time beside HumanR's clock so this is checked before any punch is stored.
Employee codes in BioTime A HumanR device PIN is a whole number. A BioTime emp_code that is numeric becomes the PIN as-is; one that equals the person's HumanR staff number is mapped automatically by the import (§8). Codes with letters cannot be PINs — HumanR counts and lists them so they can be renumbered in BioTime or mapped by hand.
Terminal serials Nothing to prepare; BioTime reports each punch's terminal_sn. Those serials appear in HumanR disabled until HR enables them (§7).
HumanR side The module flag flipped and the devices.manage permission granted to whoever will administer the sources (§4).

Quick connectivity check from the HumanR server

Run these on the HumanR server (or inside its container), not from your desk — the URL has to resolve and route from there. Replace host, user and password.

# 8.x — expect {"token":"…"}
curl -s -X POST http://biotime.example.local:8080/api-token-auth/ \
-H 'Content-Type: application/json' -d '{"username":"humanr","password":"<password>"}'
# 9.x — same shape, different path
curl -s -X POST http://biotime.example.local:8080/jwt-api-token-auth/ \
-H 'Content-Type: application/json' -d '{"username":"humanr","password":"<password>"}'
You getIt means
{"token":"…"}Credentials and licence fine; use this URL in HumanR.
HTTP 400 with non_field_errorsWrong username or password.
HTTP 403API module missing from the BioTime licence.
HTTP 404 on both pathsNot BioTime's root URL — wrong port, a sub-path, or a proxy that serves something else at /.
HTTP 301/302A redirect (usually to https). Use the redirected-to URL.
Connection refused / timeoutNetwork: firewall, wrong host, BioTime not running.

3. Create the API user in BioTime

In BioTime's web console (menu names vary slightly between 8.x and 9.x):

  1. System → User (9.x: System → User Management) → New. Username humanr (or your convention), a long random password, Active ticked. Do not make it a superuser.
  2. Give it a role that can only view: Personnel → Employee, Device → Terminal and Attendance → Transaction. Nothing else is read. If your version offers Area scoping, give it every area whose terminals should reach HumanR — a transaction outside the user's areas is simply not returned, and nothing in HumanR will tell you it exists.
  3. If the licence shows the API module as absent, stop here and sort that out first (§2).
  4. Run the curl check above with the new user. Keep the password for the HumanR form in §5 — it is entered once there, sealed at rest, and never shown again. Do not paste it into email or chat.

4. Turn the module on in HumanR

Turning a module on is three steps, and the third is the one that gets missed.

  1. Deploy a build that has the module. The sync-run table and the device source column arrive with the migrations on start.
  2. Flip the flag EnableBioTimeSync, the way flags are set for that kind of install: a cloud tenant on that instance's Configuration screen in HumanR.Office (it is rendered into the tenant's environment file; a hand-added line is overwritten by the next render); a native install in appsettings.Production.json (Features:EnableBioTimeSync = true) or its environment file (Features__EnableBioTimeSync=true); development in appsettings.Development.json. Restart the app; the worker registers at startup.
  3. Grant the permission. Admin → Roles & permissions: give Devices → manage (devices.manage) to the role that administers terminals. Admin has it already; the HR role's default set deliberately does not, and role defaults only seed a role with no claims at all, so nobody gains it on an existing install until you grant it.

Check: for a user with that role, Attendance → Devices now shows a BioTime sync link, and Maintenance → Background workers lists HumanR.Web.Services.BioTime.BioTimeSyncHostedService.

5. Add the BioTime server

Attendance → Devices → BioTime sync → Add a BioTime server.

FieldWhat to enterRules
CodeA short stable name: HQ, SITE2, FACTORY 2–20 letters, digits, - or _; upper-cased on save. Runs, terminals and the pseudo terminal BIOTIME-{code} are filed under it, so pick it once and leave it.
NameFor people: “Head office BioTime”Free text.
Server URLhttp://biotime.example.local:8080 Absolute, http/https, no path or query. The URL from §2, as the HumanR server sees it.
API usernameThe user from §3—
PasswordIts password Required when adding. On edit the box is blank and means “keep the stored one” — type a new one only to change it. Sealed with Data Protection at rest, never displayed, never logged, never in the audit log.
every … minHow often the scheduled pull runs 1–1440, default 5. Five minutes is plenty — BioTime itself only receives punches as terminals upload them.
lookback … daysHow far behind the cursor each run re-reads 0–30, default 3. This is what catches a terminal that uploaded late (§9). Raise it for a site with flaky connectivity.
page sizeRows per API page 50–5000, default 1000. Lower it only if BioTime times out on large pages.
EnabledWhether the schedule runs Leave off until Test connection passes and the terminals are enabled (§7). Sync now, Backfill and Import PINs work on a disabled source — the admin clicked.

Save → “Source HQ added. Use Test connection before enabling it.” The audit log records who added or changed a source, its host, user and settings, and whether the password changed — never the password itself.

Add a BioTime server and test the connection before turning on the sync

6. Test connection, and what each answer means

Test connection logs in, lists the terminals, counts the last 24 hours of transactions and reads the newest punch time. It stores nothing and leaves no run row, so run it as often as you like.

Success reads like:

HQ: connected (Token login) — 2 terminal(s) [Gate A, Gate B], 318 transaction(s) in the last 24 h.
BioTime's latest punch_time in the sample: 2026-10-01 08:57:12 · HumanR's clock now:
2026-10-01 09:01:40 (Asia/Dhaka) — they should be in the same zone.

Compare the two timestamps: the newest punch should be a few minutes behind HumanR's clock, not hours either side. If it is off by a round number of hours the zones differ — fix BioTime's server zone or HumanR's time zone before any punch is stored. “Token login” means 8.x, “JWT login” means 9.x.

The flash saysCauseDo this
credentials rejected — BioTime rejected the username or password.Wrong user or password, or the user is inactive.Re-check in BioTime; re-enter the password and Save.
API not licensed — BioTime answered 403 to the login…The API module is missing from the BioTime licence.Reseller. Nothing to change in HumanR.
API not licensed — BioTime answered 403 — the API module is not licensed on this BioTime, or this user may not read this data.Login worked, a later call was refused: the role lacks view on Employees, Terminals or Transactions.Fix the role (§3).
not authorised — BioTime refused the token it had just issued — the API user may be disabled.BioTime issued a token then rejected it, twice.Check the user is Active; check BioTime's own logs.
unreachable — Could not reach http://host:8080: …DNS, connection refused, TLS.Network path from the HumanR server (§2 check).
unreachable — http://host:8080 did not answer within 30 s.BioTime is up but slow, or a firewall is silently dropping.Check BioTime load; try a smaller page size; check the firewall drops versus rejects.
unexpected answer — Neither /api-token-auth/ nor /jwt-api-token-auth/ exists at this address — is it the BioTime server's root URL?Both login paths 404: wrong port, a sub-path, or a proxy.Use the root URL.
unexpected answer — … (a 3xx, HTML, or non-JSON body)A redirect — usually http → https — or a login page served by a proxy.Enter the final URL; HumanR never follows redirects.
BioTime error — …BioTime answered 5xx.BioTime's own logs; retry once it is healthy.
HQ: the stored password cannot be read on this instance — re-enter it and save.This instance's Data Protection key ring cannot open the sealed password: a database copied from another environment, or a rotated key.Re-enter the password and Save. Expected on a sandbox rebuilt from a production dump — it is why a sandbox cannot reach production BioTime by accident.

7. First sync, enable the terminals, backfill, then schedule

  1. Sync now. Flash: “HQ: sync queued — the worker picks it up within a minute.” The first run reads the last lookback days (three by default) up to now. Refresh; a run row appears with its counts.
  2. Every terminal BioTime reports is now on Attendance → Devices, disabled, tagged via BioTime HQ, named after BioTime's terminal alias (or (new) <serial> when there is none). Punches with no terminal serial — mobile-app or API punches in BioTime — file under a pseudo terminal BIOTIME-HQ, which arrives the same way. While a terminal is disabled its punches are skipped and counted (“N skipped — terminal disabled” on the run row), never stored. The sync page warns: “N terminal(s) from this source are registered but disabled, so their punches are being skipped.”
  3. Enable the terminals you want: on Attendance → Devices, give each a name and work site, tick Enabled, Save. Leave a terminal disabled to keep its punches out — a test unit, a canteen reader.
  4. Backfill the days you missed while they were disabled: pick from and to on the source card. Flash: “HQ: backfill of 24 Sep 2026 – 01 Oct 2026 queued. Terminals still disabled are skipped — enable them first.” A backfill re-reads an explicit window — at most 62 days per request, not into the future — stores what is missing, and never moves the cursor. Run it in pieces for a longer history.
  5. Tick Enabled on the source and Save. From now on the worker pulls every poll minutes on its own. The badge changes from “Disabled — no scheduled runs” to the run status.
  6. Hand over to HR for PIN mapping (§8) and a check of Attendance → Punches for one known employee against the terminal's own log.

8. Import PINs from BioTime

A punch names a BioTime employee code; HumanR needs to know which employee that is. The direct-push guide does this one PIN at a time on Unmapped PINs. With BioTime, the Import PINs from BioTime button on the source card fills the whole list in for review.

For each BioTime employee (up to 20,000 are read) HumanR proposes who they are:

MatchRule
by staff no.BioTime emp_code equals a HumanR staff number (case-insensitive). The strong match — if your BioTime codes are the staff numbers, nearly every row lands here.
by nameOtherwise, BioTime first + last name equals exactly one employee's full name, whole words, no fuzzy matching. Two employees with the same name → no proposal; pick by hand.
nonePick the employee from the search box on the row, or leave unticked.

The proposed PIN is the numeric emp_code. The Note column tells you when a row cannot be applied:

  • “cannot be a PIN” — the code has letters. HumanR PINs are whole numbers. Renumber in BioTime or map by hand later (those codes are also listed on the sync page after each run).
  • “already holds PIN … — change it on the Unmapped PINs page if that is wrong” — this employee is mapped already; left alone.
  • “PIN … is held by E-0042 (…) — conflict, not applied” — a different employee already holds that number. Resolve in BioTime or on Unmapped PINs; the import will not overwrite a mapping.

Tick the rows you accept → Apply ticked rows. Each is re-checked at apply time (employee exists, holds no other PIN, nobody else holds this one), the PIN is set, every earlier punch stored under that PIN with no employee is claimed, and those days are rolled up (back to 62 days). Flash: “14 PIN(s) set, 1,203 earlier punch(es) claimed.” — followed by “Refused: …” for anything that failed its re-check. The import shows names, codes and departments only; BioTime's employee device passwords are never read.

Nothing stops HR using the ordinary Unmapped PINs page instead or as well — it is the same mapping.

9. How a run behaves

  • Cursor. Each source remembers the furthest time it has stored, across every completed run. A scheduled or Sync now run reads from the cursor minus the lookback, up to now. The first run has no cursor and reads the last lookback days. A cursor that is somehow ahead of now (a clock that moved back) is treated as absent.
  • Day slices. The window is walked one day at a time, each day paged by page size, ordered by punch time then id. After a day's rows are saved, the cursor advances to the end of that day — so a run that dies on day 40 of 60 keeps 39, and the next run resumes from there minus the lookback. A pause of any length — an outage, a licence hold, a stopped server — is caught up on the next run without a backfill.
  • Lookback. BioTime filters transactions on punch time, not on when the terminal uploaded them. A terminal that was offline and uploads yesterday's punches today is caught because the run re-reads the last lookback days. A terminal offline longer than the lookback is the one case a scheduled run misses: raise the lookback for that site, or Backfill the gap.
  • Per row. Blank terminal_sn → pseudo terminal BIOTIME-{code}. Unknown serial → registered disabled with BioTime's alias as its name. Disabled terminal → skipped, counted. emp_code with letters → counted, first ten listed, no row. Otherwise the punch is stored with the code as its PIN, the employee holding that PIN (or none → Unmapped), the punch time as BioTime reports it, BioTime's punch_state and verify_type, and a raw line biotime:{code}:{id}… for provenance. A punch already present for that terminal, PIN and time is a duplicate and counted as “already stored”.
  • Rollup. When a run inserted anything, the days it touched are recomputed (in 31-day chunks), so Present/Absent and overtime follow within the run, not fifteen minutes later.
  • Limits. 5,000 pages per day-slice and 250,000 rows per run fail the run with a clear error — the first means BioTime ignored the page size; the second says to narrow the window with a backfill. Each HTTP call has a 30-second timeout. Login happens once per run; a token BioTime stops honouring mid-run is refreshed once.
  • One at a time. A source never runs twice at once. Sync now while a run is in progress is refused with “already running (since …)”; while one is queued, “a sync is already queued”. A Sync now stands in for that tick's scheduled run; a backfill does not.
  • Licence stand-down. While the HumanR licence is read-only or blocked the worker stands down and resumes afterwards; the cursor design means nothing is lost. It does not stand down in a sandbox's log-only mode — a pull is a read, and a sandbox cannot open a production password anyway (§6, last row).
  • Run history is kept for 30 days, except the row holding each source's cursor, and pruned every six hours. Deleting a source stops the pulls; the terminals it registered and the punches it pulled stay.

10. Monitoring

The sync page (Attendance → Devices → BioTime sync). Per source: a status badge — never run, ok, failing, running — plus Disabled — no scheduled runs and queued where they apply; URL, user, schedule; last run, last success, cursor; and the last run's counts: fetched / new / already stored / unmapped PIN(s), with skipped — terminal disabled, non-numeric code(s) and new terminal(s) when non-zero. Warnings call out an unreadable password, a failed last run with its error, and terminals awaiting enabling. Below, the last 20 runs: Started, Source, Trigger (scheduled / manual / backfill), Window, Fetched, New, Dupes, Unmapped, Skipped, Result.

Maintenance → Background workers (/ops/maintenance): lists HumanR.Web.Services.BioTime.BioTimeSyncHostedService — “Every 60 s tick; each BioTime source on its own poll interval, default 5 min (30 s after startup); prunes run history every 6 h” — with its last tick and result.

Health (/ops/health): the worker's last run and last failure. A source that fails marks the tick failed, so a wrong password left for a week is what the health watchdog exists to catch.

Logs: one information line per run — BioTime sync HQ (scheduled): fetched 318, inserted 12, duplicates 306, unmapped 0 — and a warning or error per failed source. Credentials and tokens are never logged.

Downstream: Attendance → Unmapped PINs for codes no employee holds; Attendance → Devices for terminals (the via BioTime badge marks pulled ones); Attendance → Punches for the rows.

11. Troubleshooting

SymptomCauseFix
Test connection: credentials rejectedWrong user/password, or inactive user.§6.
Test connection: API not licensed403 — the API module is not on the BioTime licence, or (after a successful login) the role cannot view that data.Reseller for the licence; §3 for the role.
Test connection: unreachableNetwork from the HumanR server, or a 30 s timeout.§2 check from the server itself.
Test connection: unexpected answer — neither login path existsNot the root URL.Root URL, right port, no sub-path.
Test connection: unexpected answer — redirect or HTMLProxy redirecting http → https, or serving a portal.Final URL.
The newest punch time and HumanR's clock differ by whole hoursZone mismatch.Align BioTime's server zone with HumanR's time zone before enabling.
Run ok, 0 new, everything “already stored”Normal after the first run — the lookback re-reads days already stored.Nothing.
Run ok, 0 fetchedNo punches in the window, or the API user's areas exclude these terminals.Compare with BioTime's own transaction list for the same window; §3 areas.
N skipped — terminal disabledTerminals registered but not yet enabled.Enable on Attendance → Devices, then Backfill the skipped days.
N unmapped PIN(s)Codes no employee holds.Import PINs from BioTime, or Unmapped PINs.
N non-numeric code(s)BioTime employee codes with letters.Renumber in BioTime (the import then maps them) or map by hand; the sample lists the first ten.
Punches land under BIOTIME-HQTransactions with no terminal serial: mobile or API punches in BioTime.Enable that pseudo terminal if you want them; leave it disabled if not.
A terminal was offline for a week; its punches are missingOffline longer than the lookback.Backfill the week; raise the lookback for that site.
“the stored password cannot be read on this instance”Different Data Protection key ring: database copied from another environment, or a rotated key.Re-enter the password.
failed — more than 250,000 rows in one runA huge window (long outage, big site).Backfill in pieces; the cursor will take over after.
failed — more than 5,000 pages in one dayBioTime is not honouring page_size.Check the BioTime version and proxy; lower the page size.
Badge failing for days, nobody noticedHealth watchdog not being watched.Put /ops/health in your monitoring; the worker tick fails whenever a source fails.
Sync now is greyed outRunning, queued, or the password is unreadable.Wait, or re-enter the password.
“BioTime sync” link missing for an adminFlag off, or the role lacks devices.manage.§4, steps 2 and 3.
After a HumanR restore from backup, the pull re-stores nothing and skips a gapThe restored cursor is older or newer than the data.Backfill the span between the backup time and now.

12. Security notes

  • The password is sealed at rest with ASP.NET Data Protection (the instance's key ring), never shown after entry, never logged, never written to the audit log — the audit entry says “password changed: yes/no”. An instance with a different key ring cannot read it and says so (§6).
  • The API token lives for one run and is not stored. A rejected token is refreshed once.
  • No redirects are followed. A redirected login would hand the credentials to whatever host the redirect names. Hence the final URL, and http/https only.
  • Outbound only, from the HumanR server to BioTime. Nothing opens towards HumanR.
  • Read-only user. HumanR only ever issues GETs after login. A view-only role is enough and is what the guide asks for.
  • BioTime's employee device-password field is never read; the import shows names, codes and departments only.
  • Who may do this: devices.manage, the permission for administering terminals. Every action is a signed form post; adding, changing and removing a source and applying a PIN import are audited.
  • Dark means dark. With the flag off the worker is not registered and no stored credential is used.
  • Prefer HTTPS to BioTime where the server supports it; on a flat internal network plain HTTP is what most BioTime installs offer, and the credential exposure is then the same as the BioTime web console's own.

13. API reference (what HumanR calls)

All calls are relative to the source's URL, Accept: application/json, 30 s timeout, no redirects.

CallPurposeWhat is read
POST /api-token-auth/ {"username","password"}Login, BioTime 8.xtoken → header Authorization: Token <token>
POST /jwt-api-token-auth/ (same body)Login, BioTime 9.x — tried when the first answers 404/405token → header Authorization: JWT <token>
GET /iclock/api/terminals/?page=&page_size=500Test connection; terminal namessn, alias
GET /iclock/api/transactions/?start_time=YYYY-MM-DD HH:MM:SS&end_time=…&page=&page_size=&limit=&ordering=punch_time,idThe punches, one day-slice at a timeid, emp_code, punch_time, punch_state, verify_type, terminal_sn, terminal_alias
GET /personnel/api/employees/?page=&page_size=500Import PINs (cap 20,000)emp_code, first_name, last_name, department

Envelopes. Both list shapes BioTime has used are accepted: {"count", "next", "previous", "data": […]} (8.x) and {"count", "next", "previous", "results": […]}. Paging follows the page number in next only — the host in next is ignored — and stops when next is null, a page is empty, or the page number does not advance. page_size and limit are both sent because versions differ on which they honour.

Field handling. emp_code and punch_state are accepted as string or number. punch_time is read to the second (YYYY-MM-DD HH:MM:SS or ISO T) and stored unchanged as the punch's time — no zone conversion, the same rule as a direct push. punch_state maps to the punch Status (0 in, 1 out, …) and verify_type to its VerifyMode, both as BioTime numbers them. An employee payload's device password field is not modelled.

Errors. 400/401 at login → credentials rejected; 403 anywhere → API not licensed (or role); 401 after a fresh re-login → not authorised; connection errors and timeouts → unreachable; 3xx, HTML or malformed JSON → unexpected answer (body quoted up to 500 characters); 5xx → BioTime error.

14. Terminals seen with BioTime so far

ModelNotes
IN01-A / IDReported the best of the four in the field — fast face + card, stable ADMS.
K40 / IDFingerprint + card; fine for small gates.
K40 ProAs K40 with a larger user capacity.
MB560-VLFace + fingerprint; needs decent lighting at the mount point.

All four stay configured exactly as they are — pointed at BioTime.

15. Rollout checklist

  1. API module confirmed on the BioTime licence (§2).
  2. Read-only BioTime user created; curl login check passes from the HumanR server (§2, §3).
  3. Firewall rule HumanR server → BioTime host:port, outbound.
  4. BioTime server zone = HumanR's time zone; terminals in step with BioTime.
  5. HumanR deployed, EnableBioTimeSync flipped for this install, devices.manage granted (§4).
  6. Source added, Enabled off; Test connection green; newest punch time within minutes of HumanR's clock (§5, §6).
  7. Sync now; terminals appear disabled on Attendance → Devices; enable the ones that count (§7).
  8. Backfill from the go-live (or history) date, ≤ 62 days per request (§7).
  9. Import PINs from BioTime; conflicts and non-numeric codes resolved (§8).
  10. Source Enabled; badge ok after the next poll; a known employee's punches match the terminal (§7, §10).
  11. /ops/health in your monitoring; whoever owns the BioTime password knows to re-enter it in HumanR when it rotates (§10, §12).

Questions this guide did not answer?

Ask us directly. We answer product questions in plain language, including the ones where the answer is “not yet”.

No credit card. No sales call required. A real login, emailed to you.

Reading the manual?

Try these screens for real

Request a demo and we'll email you a personal login to a fully loaded demo company — explore real screens with realistic data within minutes.

No credit card. No sales call required.