Base URL in local development: http://localhost:5159
Interactive reference: http://localhost:5159/swagger
No authentication. No endpoint in this API requires credentials. Every route below is fully open to any client that can reach the port. See SECURITY.md.
All request and response bodies are application/json. Route names are
case-insensitive (/api/users and /api/Users both work).
| Timestamps | ISO 8601, serialised from .NET DateTime. No timezone offset is stored — values are whatever the terminal's clock reported. |
| Nullability | Almost every field is nullable. A missing value means "not recorded", not "zero". |
| Errors | 404 Not Found for unknown ids, 400 Bad Request with a plain-text message on the one validated path (POST /api/Attendance). No structured error envelope. |
| Success | Write endpoints return 200 OK with the persisted entity (not the DTO). DELETE returns 200 OK with an empty body. |
Employees. The EnrollNumber field links an employee to their identity on the
physical terminals.
Lists every employee with their department name joined in.
curl http://localhost:5159/api/Users[
{
"id": 1,
"enrollNumber": 35,
"name": "Rakoto Aina",
"departmentId": 2,
"departmentName": "Production"
}
]Takes no query parameters. FrontZK calls
GET /api/Users?departmentId=…ingetUsersByDepartment(); the parameter is ignored and the full list is returned. The frontend then filters inuseUsersByDepartment.
{id} is the database User.Id, not the enroll number.
200 OK with a single UserDto, or 404 Not Found.
curl -X POST http://localhost:5159/api/Users \
-H 'Content-Type: application/json' \
-d '{"name":"Rakoto Aina","enrollNumber":35,"departmentId":2}'Reads name, enrollNumber and departmentId. Any id or departmentName in
the body is ignored.
Returns 200 OK with the created User entity, including its assigned id.
EnrollNumberis not validated for uniqueness, and no unique index exists on the column. Two employees sharing an enroll number will silently break attendance attribution — see ARCHITECTURE.md § 4.
Full replacement of name, enrollNumber, departmentId. 404 if {id} is
unknown.
200 OK on success, 404 if unknown.
No cascade configuration is declared. Deleting an employee who has attendance rows will fail at the database level on the foreign-key constraint.
[
{ "id": 1, "name": "Administration" },
{ "id": 2, "name": "Production" }
]200 OK with one DepartmentDto, or 404.
curl -X POST http://localhost:5159/api/Departments \
-H 'Content-Type: application/json' \
-d '{"name":"Logistique"}'Name is required and capped at 100 characters by the model.
Replaces name. 404 if unknown.
200 OK, or 404. Same foreign-key caveat as employees: departments with
employees attached cannot be deleted.
One row per work session. CheckIn set with CheckOut still null means the
session is open.
Returns every attendance record in the database, with the employee name joined.
[
{
"id": 412,
"userId": 1,
"userName": "Rakoto Aina",
"date": "2026-09-07T00:00:00",
"checkIn": "2026-09-07T07:52:14",
"checkOut": "2026-09-07T16:31:02",
"checkInMode": 0,
"checkOutMode": 0
}
]No filtering is implemented. FrontZK calls this endpoint with
?date=,?userId=,?startDate=&endDate=—GetAll()accepts no parameters and ignores all of them. Every dashboard view downloads the full table and filters in the browser. Acceptable at 41 employees; not at 500.
200 OK with one AttendanceDto, or 404.
This endpoint does not behave like the others. Read this before calling it.
The userId field in the request body is interpreted as the terminal's
enroll number, not as User.Id. The controller resolves it to a real
employee and stores the correct foreign key:
var user = await _context.Users.FirstOrDefaultAsync(u => u.EnrollNumber == dto.UserId);
if (user == null)
return BadRequest($"Aucun user avec EnrollNumber {dto.UserId}");This exists because ZUtility — the only production caller — reads enroll numbers off the terminals and has no way to know database identities. Rationale in ARCHITECTURE.md § 3④.
# 35 here is an ENROLL NUMBER, not a User.Id
curl -X POST http://localhost:5159/api/Attendance \
-H 'Content-Type: application/json' \
-d '{
"userId": 35,
"date": "2026-09-07T00:00:00",
"checkIn": "2026-09-07T07:52:14",
"checkOut": null,
"checkInMode": 0,
"checkOutMode": null
}'| Response | When |
|---|---|
200 OK + Attendance entity |
Employee found. Note the returned userId is the real User.Id — it will differ from what you sent. |
400 Bad Request + "Aucun user avec EnrollNumber {n}" |
No employee carries that enroll number. |
Not idempotent. There is no natural-key constraint. Posting the same punch twice creates two rows. This is why a ZUtility restart — which resets its in-memory cursor and re-reads each device buffer from the beginning — duplicates history.
Full replacement of date, checkIn, checkOut, checkInMode,
checkOutMode, userId.
Unlike
POST,PUTwritesdto.UserIdstraight into the foreign key without any enroll-number lookup. The same field name means an enroll number onPOSTand aUser.IdonPUT. Passing an enroll number here will corrupt the row's ownership.
200 OK, or 404.
checkInMode and checkOutMode carry the terminal's verification method,
straight from the ZKTeco SDK's verifyMode. Mirrored in FrontZK as
checkModeLabels (lib/types.ts).
| Value | Method |
|---|---|
0 |
Fingerprint |
1 |
Card |
2 |
Password |
3 |
Face |
4 |
Manual |
Values outside this range are stored as-is; nothing validates the range.
GET /api/WeatherForecast is still live — it comes from the ASP.NET Core
project template and returns five random forecasts. It carries no data of
interest and should be deleted.
| Caller | Location | Uses |
|---|---|---|
| ZUtility | Services/AttendanceService.PushAsync |
POST /api/Attendance |
| FrontZK | lib/api.ts |
GET on all three resources |