Tenant API v1 — Attendance
Base path: /api/tenant/v1
Middleware: auth:tenant-api, tenant.user, not.suspended, verified, module:attendance, plus can:attendance.*.
Catalog version: 1.10.0.
Stats
GET /attendance-records/stats
Same filters as list (minus pagination/sort). Payload:
total, present, absent, half_day, remote, late, plus open presence counts checked_in_open, on_site_open, remote_open (today, checked in, not checked out).
Daily email digests
Scheduled (not HTTP): attendance:send-daily-reports every 5 minutes. Tenant settings (Attendance group):
| Key | Default | Purpose |
|---|---|---|
attendance_late_report_enabled | false | Send late-today digest after local time |
attendance_late_report_time | 09:30 | Workspace-local H:i gate |
attendance_daily_report_enabled | false | Send yesterday digest after local time |
attendance_daily_report_time | 08:00 | Workspace-local H:i gate |
Recipients: non-suspended Owners / Admins get the full company digest; department managers (manager_id on an active department) get only employees in their managed departments. Spatie manager without department assignment does not receive digests. Requires attendance.view. Notification types attendance.late.digest and attendance.yesterday.digest (database + mail). Idempotent via daily_summary_deliveries.
Today / self check-in
| Method | Path | Permission |
|---|---|---|
| GET | /attendance-records/today | Linked active employee + self-check enabled, or attendance.view |
| POST | /attendance-records/check-in | Linked active employee + self-check enabled (no attendance.create) |
| POST | /attendance-records/check-out | Linked active employee + self-check enabled (no attendance.update) |
GET /attendance-records/today
Returns today’s record for the linked employee (or null) plus meta:
self_check_enabled,require_late_reason,timezone,todaythresholds.on_site/thresholds.remote—{ start, grace_minutes, is_late_now }
POST /attendance-records/check-in
Body: work_mode (on_site|remote), optional reason_id / reason_notes, optional latitude / longitude.
Late on-site uses office start + grace; late remote uses remote start + grace. When late and attendance_require_late_reason is on, reason_id is required; Other requires reason_notes. Status is present/late (on-site) or late with work_mode=remote when remote is late; on-time remote stays remote. Server stamps check_in_ip; coordinates are best-effort.
POST /attendance-records/check-out
Body: optional reason_id / reason_notes, optional latitude / longitude. Check-out reason is optional; Other requires notes. Server stamps check_out_ip.
Attendance reasons
| Method | Path | Permission |
|---|---|---|
| GET | /attendance-reasons | attendance.view |
| POST | /attendance-reasons | attendance.update (admin/owner catalog) |
| GET | /attendance-reasons/{attendanceReason} | attendance.view |
| PUT | /attendance-reasons/{attendanceReason} | attendance.update (admin/owner) |
| DELETE | /attendance-reasons/{attendanceReason} | attendance.update (admin/owner) |
Kinds: check_in_late, check_out. Each kind has a protected Other row (is_other).
Workspace holidays
| Method | Path | Permission |
|---|---|---|
| GET | /attendance-holidays | attendance.view |
| POST | /attendance-holidays | attendance.update (admin/owner catalog) |
| GET | /attendance-holidays/{attendanceHoliday} | attendance.view |
| PUT | /attendance-holidays/{attendanceHoliday} | attendance.update (admin/owner) |
| DELETE | /attendance-holidays/{attendanceHoliday} | attendance.update (admin/owner) |
Body: date (required, unique per tenant), optional name. List filters: year, date_from, date_to, search. Holidays are never treated as Absent for matrix inference or payroll deduction.
Month matrix & export
| Method | Path | Permission |
|---|---|---|
| GET | /attendance-records/matrix | attendance.view |
| GET | /attendance-records/export | attendance.export |
Query params: month (YYYY-MM, required), optional search, department_id, employee_status (default active), and for export format (csv|xlsx, default csv).
Matrix JSON: month, timezone, days[] (date, label, is_today, is_work_day, is_holiday, holiday_name), holidays[] (id, date, name), employees[] (id, employee_number, full_name, job_title, departments[], status, days map of YYYY-MM-DD → { status, record_id, source, holiday_name }). source is record | inferred | holiday. Missing day keys mean off day / future (not inferred). Past/today work days with no record and not a holiday get status=absent, source=inferred. Visibility matches list (AttendanceRecordPolicy::visibleEmployeeIds).
Export: Same filters as matrix. Sheet starts with a legend row (P/A/L/R/HD/H), then header columns ID, Employee number, Name, Job title, Department, Employee status, then one column per calendar day. Filename attendance_MM_YYYY.csv or .xlsx.
Attendance records CRUD
| Method | Path | Permission |
|---|---|---|
| GET | /attendance-records | attendance.view |
| GET | /attendance-records/stats | attendance.view |
| GET | /attendance-records/matrix | attendance.view |
| GET | /attendance-records/export | attendance.export |
| POST | /attendance-records | attendance.create |
| GET | /attendance-records/{attendanceRecord} | attendance.view |
| GET | /attendance-records/{attendanceRecord}/timeline | attendance.view |
| PUT | /attendance-records/{attendanceRecord} | attendance.update |
| DELETE | /attendance-records/{attendanceRecord} | attendance.delete |
| POST | /attendance-records/{attendanceRecord}/restore | attendance.restore |
| DELETE | /attendance-records/{attendanceRecord}/force | attendance.force.delete |
POST /attendance-records
Body: employee_id, date (required; unique per employee), required change_reason (min 5), optional check_in / check_out (HH:MM or HH:MM:SS), status (present|absent|half_day|remote|late, default present), notes, optional latitude / longitude. Server stamps check-in/out IP when times are set (client IPs are ignored).
Date rules: date other than workspace today requires attendance.backdate (or workspace owner / superadmin). Actors without that privilege receive a date validation error.
Login side-effect (optional): when attendance_auto_check_in_on_login is true, successful POST /auth/login, passkey login, or 2FA challenge may upsert today’s on-site check-in for the user’s active linked employee (optional latitude / longitude on those payloads). Default is false — staff must use check-in. If the login would be late and attendance_require_late_reason is true, auto check-in is skipped (no reason can be collected on login).
PUT /attendance-records/{attendanceRecord}
Partial update of the same fields (plus work mode / reason fields for managers). change_reason is required (min 5). Optional latitude / longitude re-stamp location when check-in/out times change. Changing date to a day other than today requires attendance.backdate (or owner); leaving a past date unchanged does not.
GET /attendance-records/{attendanceRecord}/timeline
Newest-first activity rows: type (created|updated|checked_in|checked_out|deleted|restored), description, change_reason, properties (before/after diffs), ip, coordinates, actor, created_at.