Research & evidence
What was actually verified, how, and — just as importantly — what was not.
Why this page exists
An integration guide that presents tested and untested behaviour in the same confident voice is misleading in the place it matters most. This page separates the two, so anyone deploying Biotime knows which parts rest on evidence and which rest on documentation.
What was tested against a live server
The module was designed against a real, running ZKBio TimeCloud tenant, not against vendor documentation. The captured contract ships with the module:
| Artifact | Contents |
|---|---|
specs/openapi.json |
113 documented paths across the personnel, attendance, device, access-control, GPS, workflow and report namespaces. Every path slash-terminated. Declares the auth scheme as “Use exact format: JWT <access token>. Do not use Bearer.” |
specs/postman_collection.json |
The same routes as a runnable collection, organised into nine folders — including one explicitly named “Known Non-Working / Do Not Use”. |
specs/verification_summary.json |
The live run: 110 routes attempted, 110 verified, on 2026-08-21. Records each route's HTTP status, vendor response code, and the field names actually returned. |
The tenant carried 258,398 attendance transactions, 185 employees, 4 terminals, 15 departments and 5 areas. That scale is why the sync is incremental, cursor-driven and bounded rather than a simple loop — a full walk at 200 rows per page is roughly 1,300 requests.
Field names come from the live response, not from a guess
The stored columns mirror the field names the server actually returned. For attendance
punches those were: id, emp_code, first_name,
last_name, dept_code, dept_name,
position_code, position_name, punch_date,
punch_time, punch_state, verify_type,
work_code, latitude, longitude,
gps_location, terminal_sn, terminal_alias,
area_alias, upload_time and capture_url.
Several fields that early design drafts assumed would exist — a source
marker, an is_attendance flag, a checksum, GPS accuracy — are not in
the feed and were removed rather than stored empty.
Known-broken paths
Confirmed to fail on this server generation. Biotime never calls them, and its profile-detection deliberately avoids replaying credentials against them:
| Path | Result | Use instead |
|---|---|---|
/api/jwt-api-token-auth/ | 405 | /web/oauth/api/v2/user_sign_in/ |
/web/api/jwt-api-token-auth/ | Present but misconfigured | /web/oauth/api/v2/user_sign_in/ |
/web/oauth/api/v2/sign_in/ | 404 | /web/oauth/api/v2/user_sign_in/ |
/web/att/api/v2/employee_schedules/ | 404 | /web/att/api/v2/schedules/ |
/web/att/api/v2/calculations/ | 404 | POST …/calculations/calculate/ |
/web/device/api/v2/commandlogs/ | 404 | /web/device/api/v2/commands/ |
/web/device/api/v2/terminallogs/ | 404 | No replacement |
/web/device/api/v2/employees/ | 404 | /web/personnel/api/v2/employees/ |
What was NOT tested
Also untested at the time of writing: behaviour against a server under heavy sustained
rate-limiting (the handling exists and is classified as BT-RATE, but the
back-off has not been exercised at length), and any BioTime deployment that reports body
temperature — cloud servers do not send that field at all.
ERPat-side evidence
The design also rests on facts checked directly in the ERPat codebase rather than assumed:
| Question | What the code says | Consequence |
|---|---|---|
| Are ERPat employee ID numbers unique? | No. The employee model states outright that there is no uniqueness constraint, that duplicates already exist in real data, and that a collision must never block a save. | A multi-candidate match is a conflict, never a guess. This single fact shapes the whole binding design. |
| Which HTTP client does ERPat use? | Native cURL. The house client states plainly that the repository carries no first-party Guzzle. | Biotime uses cURL and adds no new dependency. |
| How are secrets stored elsewhere? | Packed AES-256-GCM with the ciphertext bound to its row, as used by the Webmail module. | Biotime reuses that proven pattern rather than inventing one. |
| Who owns biometric enrollment records? | The Kiosk module, whose shared table treats a NULL enrollment label as a wildcard matching every device. | Biotime writes only under its own dedicated label — writing a wildcard would silently widen Kiosk's own matching. |
| Where would attendance import have to go? | Through the Kiosk timelogs pipeline, which builds records from paired taps. The live clock-in path is unsuitable for imports. | Import is deferred to its own phase rather than bolted on. |
A defect this evidence caught
Worth recording, because it shows why the field-level testing mattered. Punch timestamps
arrive as plain strings, and a date library will happily accept a
zero date (0000-00-00 00:00:00) without complaint — silently
producing a timestamp nearly two thousand years in the past. Such a row would have sorted
to the very top of every listing and every export.
The module now rejects any timestamp before 1970 outright, and a test pins that behaviour.
Reading the source yourself
The full design record, including the reasoning behind every decision and the alternatives
rejected, is in modules/Biotime/specs/biotime-implementation-plan.md. It
carries a companion status document and a clickable UI prototype.