Integrate attendance with payroll and HR
A REST API over HTTPS with JSON bodies. Every organisation's data is isolated; a key only ever sees the organisation that created it. Base URL: https://attendance.circuvent.com/api/v1. Import openapi.json into Postman, Insomnia or a client generator.
Authentication
An administrator creates keys in the console under Developers → API keys, choosing scopes. Send the key as a bearer token:
curl -H "Authorization: Bearer cva_xxxxxxxxxx_…" \ https://attendance.circuvent.com/api/v1/me
Conventions
- Days are YYYY-MM-DD in the site's time zone; instants are ISO-8601 UTC.
- Employees can be addressed by UUID, code:E001 or ext:HR-991.
- Lists that can grow return next_cursor; pass it back as ?cursor=.
- POSTs accept Idempotency-Key (24 h); replays return the original response.
- Rate limit per key (default 600/min). Watch X-RateLimit-Remaining; on 429 wait Retry-After seconds.
- Add format=csv to the register, punches and payroll export for spreadsheets.
Errors
HTTP/1.1 400 Bad Request
{ "error": { "code": "invalid_request", "message": "Request validation failed",
"details": [{ "path": "from_day", "message": "must be YYYY-MM-DD" }],
"request_id": "5f0c…" } }Codes: 400 invalid, 401 missing/invalid key, 403 missing scope, 404 not found, 409 conflict (duplicate code or card), 422 idempotency key reused, 423 locked payroll period, 429 rate limited.
Typical payroll integration
- Sync employees from your HRMS with POST /employees/bulk (set external_id to your id).
- Push approved leave with POST /leaves (set external_id to make it idempotent).
- After month end, call GET /payroll/export?from=…&to=…. Use payable_days and lop_days.
- Once payroll is processed, lock the period (POST /payroll/periods/{id}/lock) so the figures cannot change.
Webhooks
Each delivery is a POST with a JSON envelope { id, type, created_at, tenant_id, data }. Respond 2xx within 10 s. Failures are retried with back-off for about three days; an endpoint that keeps failing is disabled and shown in the console.
// Node.js verification
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const mac = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1));
}Events: punch.created, attendance.day.updated, attendance.day.finalized, employee.*, leave.*, payroll_period.locked|unlocked, device.online|offline|alarm. The same events can be polled from GET /events.
Endpoints
Employees
get/employees/hrms-syncHRMS sync status
Whether HRMS sync is configured on this server, and the result of the last run. Required scopes: employees:read
post/employees/hrms-syncSync employees from HRMS
Mirrors the HRMS directory for the organisation's verified email domains: matches by HRMS id, code, then work email; creates departments; deactivates linked people who left HRMS. Hand-made employees are untouched. Also runs every 30 minutes. If any verified domain fails, or every domain returns an empty roster while linked employees exist, the response has ok: false, the failure is stored as the last sync, and nobody is changed. Required scopes: employees:write
get/employeesList employees
Keyset-paginated by (updated_at, id). For incremental sync pass updated_since and follow next_cursor. Required scopes: employees:read
post/employeesCreate employee
Required scopes: employees:write
post/employees/bulkUpsert up to 1000 employees
Matches on employee_code. Accepts department_name (created if missing), site_name and card_number. Each row succeeds or fails on its own. Required scopes: employees:write
get/employees/{id}Get employee
Required scopes: employees:read
patch/employees/{id}Update employee
Required scopes: employees:write
delete/employees/{id}Terminate employee
Soft delete: status becomes terminated and cards are revoked. History is kept. Required scopes: employees:write
get/employees/{id}/credentialsList cards
Required scopes: employees:read
post/employees/{id}/credentialsIssue a card
Required scopes: employees:write
post/credentials/{id}/revokeRevoke a card
Required scopes: employees:write
Attendance
get/punchesList punches
Required scopes: attendance:read
post/punchesRecord punches
One punch, or { "punches": [...] } with up to 500. Supply external_id to make retries safe. Required scopes: attendance:write
get/attendance/dailyDaily register
One row per employee per day with status, first in, last out and minutes. Days are final after 06:00 the next morning (site time). Required scopes: attendance:read
patch/attendance/daily/{employeeId}/{day}Correct a day
Manual corrections are never overwritten by recomputation. { "revert": true } hands the day back to the automatic register. Returns 423 inside a locked payroll period. Required scopes: attendance:write
post/attendance/recomputeRecompute the register
Required scopes: attendance:write
get/attendance/summaryAttendance summary per employee
Required scopes: attendance:read
get/attendance/liveToday's live board
Required scopes: attendance:read
Analytics
get/analytics/overviewAttendance analytics overview
KPIs with deltas against the previous period of equal length, a daily series, weekday pattern, status totals, department breakdown, arrival-time histogram, punch heatmap (weekday × hour, granted scans) and leaderboards. Attendance rate = (present + late + ½ half) ÷ (present + late + half + absent); leave, holiday, weekend and unknown days are excluded. Punctuality = present ÷ (present + late). Times are minutes since local midnight in the site's zone. format=csv returns the daily series. Required scopes: attendance:read
get/analytics/employees/{id}One employee's analytics
KPIs against the previous period and the employee's team (department, else site; counts per person), register days for a calendar, daily/weekly/weekday series, up to 100 recent scans and leaves overlapping the range. Required scopes: attendance:read
get/analytics/devicesTerminal analytics
Per terminal: online state, last seen, firmware, RSSI, queue depth, scans per local day (granted, refused, uploaded from the offline queue) and refusal reasons. Defaults to the last 14 days. Required scopes: devices:read
Reports
get/reports/musterMuster roll
Employee × day grid for a month. Codes: P present, L late, H half day, A absent, LV leave, HO holiday, WO weekly off, - not yet known; blank = no register row. Includes per-employee totals. format=csv for a spreadsheet. Required scopes: attendance:read
get/reports/{kind}Exception and summary reports
late: late arrivals. overtime: days with overtime. absent: absences. early-exit: left before the shift ended. missed-punch: a single scan or no out-scan (shift end assumed), today excluded. summary: per-employee totals and rates. Row reports return at most 20,000 rows (truncated: true beyond that). format=csv for a spreadsheet. Required scopes: attendance:read
Payroll
get/payroll/exportPayroll export
Payroll-ready totals per employee for a period: present, absent, paid and unpaid leave, loss-of-pay and payable days, worked and overtime hours. locked tells you whether the figures are frozen. Required scopes: payroll:read
post/payroll/periods/{id}/lockLock a payroll period
Required scopes: payroll:write
post/payroll/periods/{id}/unlockUnlock a payroll period
Required scopes: payroll:write
get/payroll/periodsList payroll
Required scopes: payroll:read
post/payroll/periodsCreate
Required scopes: payroll:write
get/payroll/periods/{id}Get one
Required scopes: payroll:read
patch/payroll/periods/{id}Update
Required scopes: payroll:write
delete/payroll/periods/{id}Delete
Required scopes: payroll:write
Leaves
get/leavesList leaves
Required scopes: attendance:read
post/leavesCreate
Required scopes: attendance:write
get/leaves/{id}Get one
Required scopes: attendance:read
patch/leaves/{id}Update
Required scopes: attendance:write
delete/leaves/{id}Delete
Required scopes: attendance:write
Holidays
get/holidaysList holidays
Required scopes: settings:read
post/holidaysCreate
Required scopes: settings:write
get/holidays/{id}Get one
Required scopes: settings:read
patch/holidays/{id}Update
Required scopes: settings:write
delete/holidays/{id}Delete
Required scopes: settings:write
Organisation
get/sitesList organisation
Required scopes: settings:read
post/sitesCreate
Required scopes: settings:write
get/sites/{id}Get one
Required scopes: settings:read
patch/sites/{id}Update
Required scopes: settings:write
delete/sites/{id}Delete
Required scopes: settings:write
get/schedulesList organisation
Required scopes: settings:read
post/schedulesCreate
Required scopes: settings:write
get/schedules/{id}Get one
Required scopes: settings:read
patch/schedules/{id}Update
Required scopes: settings:write
delete/schedules/{id}Delete
Required scopes: settings:write
get/departmentsList organisation
Required scopes: employees:read
post/departmentsCreate
Required scopes: settings:write
get/departments/{id}Get one
Required scopes: employees:read
patch/departments/{id}Update
Required scopes: settings:write
delete/departments/{id}Delete
Required scopes: settings:write
Devices
get/devicesList terminals
Required scopes: devices:read
post/devicesRegister a terminal
Returns a one-time claim_code to enter on the terminal. Required scopes: devices:write
get/devices/{id}Get terminal
Required scopes: devices:read
patch/devices/{id}Update terminal
Required scopes: devices:write
delete/devices/{id}Remove terminal
Required scopes: devices:write
post/devices/{id}/claim-codeRe-provision (new claim code)
Required scopes: devices:write
post/devices/{id}/commandsSend a command
Required scopes: devices:write
Webhooks
get/webhooksList webhook endpoints
Required scopes: webhooks:manage
post/webhooksCreate webhook endpoint
Returns the signing secret once. Deliveries carry Circuvent-Signature: t=<unix>,v1=<hex HMAC-SHA256 of "<t>.<body>">. Required scopes: webhooks:manage
get/webhooks/{id}Get endpoint
Required scopes: webhooks:manage
patch/webhooks/{id}Update endpoint
Required scopes: webhooks:manage
delete/webhooks/{id}Delete endpoint
Required scopes: webhooks:manage
post/webhooks/{id}/testSend a test ping
Required scopes: webhooks:manage
post/webhooks/{id}/rotate-secretRotate signing secret
Required scopes: webhooks:manage
get/webhooks/{id}/deliveriesRecent deliveries
Required scopes: webhooks:manage
get/eventsEvent feed
The webhook events as a pull feed (7-day retention). Poll with next_cursor. Required scopes: attendance:read
Meta
get/meWho am I
Returns the organisation and scopes of the calling key.
get/me/attendanceMy attendance
Signed-in console sessions only (scope self:read, never an API key). The caller's own register, punches, leaves, holidays and totals for a month, matched to the employee record by work email.
get/organizationOrganisation settings and usage
Required scopes: settings:read
get/audit-logAudit log
Required scopes: audit:read
Terminals (device protocol)
Circuvent terminals talk to /api/device/v1 over HTTPS with their own per-device token, obtained once by exchanging the claim code shown in the console. They poll the allow-list, upload queued scans in batches and send a heartbeat that returns configuration and commands. Integrators do not need this API; see docs/device-protocol.md in the repository if you build your own terminal.