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

  1. Sync employees from your HRMS with POST /employees/bulk (set external_id to your id).
  2. Push approved leave with POST /leaves (set external_id to make it idempotent).
  3. After month end, call GET /payroll/export?from=…&to=…. Use payable_days and lop_days.
  4. 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.