Attendance Sanchalan
Sanchalan Docs
API reference

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/redirect bounces to sb-iam, /api/auth/sso/callback consumes the code and starts a web session (no API token returned).
  • Mobile SSO: POST /api/auth/sso/mobile/redirect returns the IdP URL; POST /api/auth/sso/mobile/exchange trades code+verifier for a Sanctum personal-access-token.

Endpoint index

MethodPathAuthPurpose
GET/api/healthznoneLiveness probe.
GET/api/auth/modenoneTells the client whether SSO is enabled and where to redirect.
GET/api/auth/sso/redirectsessionWeb SSO start.
GET/api/auth/sso/callbacksessionWeb SSO finish.
POST/api/auth/sso/mobile/redirectnoneMobile SSO start (PKCE).
POST/api/auth/sso/mobile/exchangenoneMobile SSO finish — returns Sanctum PAT.
v1 — self-service
GET/api/v1/mebearerAuthenticated user + linked employee.
GET/api/v1/holidays?year=YYYYbearerYear holidays.
POST/api/v1/devices/registerbearerIdempotent register / re-register a BYOD device.
GET/api/v1/devicesbearerList devices for the current employee.
POST/api/v1/devices/{id}/deactivatebearerOperator-revoke a device.
POST/api/v1/punchbearer + 30/minRecord an IN/OUT punch.
GET/api/v1/punches/todaybearerToday's punches for current employee.
GET/api/v1/punches/month/{yyyy-mm}bearerAll punches in a month.
POST/api/v1/leavesbearerApply for CCS leave.
GET/api/v1/leavesbearerMy recent leave applications.
GET/api/v1/leaves/balances?year=YYYYbearerYear balances by leave type.
POST/api/v1/leaves/{id}/approvebearer + leave-approverDecide a pending leave (decision: approve|reject).
POST/api/v1/toursbearerApply for tour / out-of-office.
GET/api/v1/toursbearerMy recent tours.
POST/api/v1/tours/{id}/decidebearer + leave-approverDecide a pending tour.
v1 — admin (RBAC: admin-attendance)
GET/api/v1/admin/holidaysbearer + adminHolidays admin view (filterable).
GET/api/v1/admin/geo-fencesbearer + adminFence catalog.
GET/api/v1/admin/reports/attendancebearer + reportsDate-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

ScopeLimit
v1/* (default)60 / minute / token
POST /v1/punch30 / minute / token (overrides default)
auth/sso/callback30 / minute / IP
auth/sso/mobile/*30 / minute / IP
nginx host edgelimit_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 returns 422 verdict=duplicate.