API reference
Full interactive OpenAPI 3.0 spec is at /api/documentation (Swagger UI). Raw JSON: /docs.
Authentication
All /api/v1/* endpoints require a Sanctum bearer token in Authorization: Bearer <token>.
- Dev: tokens minted via tinker (see Prototype).
- Web SSO:
GET /api/auth/sso/redirectbounces to sb-iam,/api/auth/sso/callbackconsumes the code and starts a web session (no API token returned). - Mobile SSO:
POST /api/auth/sso/mobile/redirectreturns the IdP URL;POST /api/auth/sso/mobile/exchangetrades code+verifier for a Sanctum personal-access-token.
Endpoint index
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/healthz | none | Liveness probe. |
| GET | /api/auth/mode | none | Tells the client whether SSO is enabled and where to redirect. |
| GET | /api/auth/sso/redirect | session | Web SSO start. |
| GET | /api/auth/sso/callback | session | Web SSO finish. |
| POST | /api/auth/sso/mobile/redirect | none | Mobile SSO start (PKCE). |
| POST | /api/auth/sso/mobile/exchange | none | Mobile SSO finish — returns Sanctum PAT. |
| v1 — self-service | |||
| GET | /api/v1/me | bearer | Authenticated user + linked employee. |
| GET | /api/v1/holidays?year=YYYY | bearer | Year holidays. |
| POST | /api/v1/devices/register | bearer | Idempotent register / re-register a BYOD device. |
| GET | /api/v1/devices | bearer | List devices for the current employee. |
| POST | /api/v1/devices/{id}/deactivate | bearer | Operator-revoke a device. |
| POST | /api/v1/punch | bearer + 30/min | Record an IN/OUT punch. |
| GET | /api/v1/punches/today | bearer | Today's punches for current employee. |
| GET | /api/v1/punches/month/{yyyy-mm} | bearer | All punches in a month. |
| POST | /api/v1/leaves | bearer | Apply for CCS leave. |
| GET | /api/v1/leaves | bearer | My recent leave applications. |
| GET | /api/v1/leaves/balances?year=YYYY | bearer | Year balances by leave type. |
| POST | /api/v1/leaves/{id}/approve | bearer + leave-approver | Decide a pending leave (decision: approve|reject). |
| POST | /api/v1/tours | bearer | Apply for tour / out-of-office. |
| GET | /api/v1/tours | bearer | My recent tours. |
| POST | /api/v1/tours/{id}/decide | bearer + leave-approver | Decide a pending tour. |
| v1 — admin (RBAC: admin-attendance) | |||
| GET | /api/v1/admin/holidays | bearer + admin | Holidays admin view (filterable). |
| GET | /api/v1/admin/geo-fences | bearer + admin | Fence catalog. |
| GET | /api/v1/admin/reports/attendance | bearer + reports | Date-range pivot of punch verdicts. |
Request / response shapes
POST /api/v1/punch
{
"device_uuid": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
"punch_type": "IN", // or "OUT"
"punched_at": "2026-04-27T09:00:00Z",
"lat": 28.6175,
"lng": 77.2085,
"accuracy_m": 12,
"ssid_seen": "RSS-Wifi-Internal", // optional but boosts indoor confidence
"nonce": "0123456789abcdef0123456789abcdef", // 32 chars, hex(16)
"signature": "rO4...==", // ECDSA P-256 SHA-256 raw r||s, std-base64
"anti_spoof": {
"mock_location": false,
"rooted": false,
"emulator": false,
"vpn": false
}
}
// 201 — accepted
{ "punch_id": 12345, "verdict": "accepted", "reason": [], "recorded": true }
// 422 — rejected (still recorded for audit)
{ "verdict": "rejected_geofence",
"reason": { "geofence": "Outside any allowed geo-fence or Wi-Fi SSID not on allowlist" } }
POST /api/v1/leaves
{
"leave_type": "CL", // CL | EL | HPL | COMMUTED | CCL | RH
"start_date": "2026-05-01",
"end_date": "2026-05-01",
"is_half_day": false,
"reason": "personal"
}
Common error envelope
// 401
{ "message": "Unauthenticated." }
// 403
{ "message": "Forbidden: 'admin-attendance' requires one of: admin, hr_admin, ..." }
// 422
{ "message": "...", "errors": { "field": ["..."] } }
Rate limits
| Scope | Limit |
|---|---|
| v1/* (default) | 60 / minute / token |
| POST /v1/punch | 30 / minute / token (overrides default) |
| auth/sso/callback | 30 / minute / IP |
| auth/sso/mobile/* | 30 / minute / IP |
| nginx host edge | limit_req zone=attendance burst=40 nodelay at 20 r/s |
Idempotency
POST /v1/devices/register— keyed on(employee_id, device_uuid); same UUID re-registering replaces the row.POST /v1/punch— keyed on(employee_id, nonce)for accepted verdicts. Same nonce replay returns422 verdict=duplicate.
