# ADMS API reference

Everything the portal does goes through this API, so anything on screen can be done by another system.

## Calling it

- **Base address:** the server's own address, for example `https://adms.example.com`. Every path below starts with `/api`.
- **Sign in:** send `Authorization: Bearer <token>`. Tokens are created on the portal's Access page and shown once.
- **Organisations:** one server holds many client organisations, each walled off from the others: its branches, departments, people, terminals, accounts, tokens, webhooks, time periods and settings. Person numbers, branch codes, department numbers and period numbers are unique inside an organisation only. A token belongs to one organisation and acts on it; nothing of another organisation exists to it.
- **Super admin:** an admin token or account with no organisation. It names the organisation a request is about in the header `x-organisation: <id>` (the live stream, which cannot carry a header, takes `?org=<id>`). Without it, the one organisation on the server is assumed when there is exactly one; otherwise an organisation-owned call answers `400 organisation_required`, and only the server-level calls work: organisations, the whole terminal list and the approval queue, server settings, the logs, firmware, webhooks. A header naming no organisation answers `400 unknown_organisation`.
- **Roles:** `admin` can do everything in its organisation (a super admin, everything on the server). `operator` runs terminals, people and attendance. `viewer` can only read.
- **Branch limits:** a token limited to branches only sees and acts on the terminals in them, the people on those terminals or in those branches' departments, and the punches made there. Anything else answers `404`, as if it did not exist.
- **Address limits:** a token tied to addresses answers `403 ip_not_allowed` from anywhere else.
- **From a web page on another site:** allowed only for origins listed under Settings, and only with a token, never the session cookie.
- **Bodies** are JSON. **Times:** `time_local` is the terminal's own clock (`YYYY-MM-DD HH:MM:SS`); `time` and `occurred_at` are UTC.
- **Errors** are JSON: `{ "error": "<code>", "message": "<plain words>" }`. `400` bad request, `401` no or bad token, `403` not allowed, `404` not found or out of reach, `409` not possible in the current state.

## Punches and events

| Call | What it does |
|---|---|
| `GET /api/events?after=<id>&limit=&sn=&type=` | Every event in order. Keep the last `id` and ask again with `after=` to follow along. |
| `GET /api/stream` | The same events live, as server-sent events. |
| `GET /api/attendance?from=&to=&pin=&sn=&department_id=&q=&source=&repeats=1` | Punch records by terminal-local day. Repeat punches are left out unless `repeats=1`. |
| `GET /api/attendance/daily?from=&to=` | One row per person per day: first in, last out, punches, hours. |
| `GET /api/attendance/present?date=` | Who is in now. |
| `POST /api/attendance` `{pin, time_local, note, status?, sn?}` | Add a record by hand. A reason (`note`) is required. A token may do this only if it was created with that permission. |
| `DELETE /api/attendance/:id` | Delete a record added by hand (same permission). Records from terminals cannot be deleted. |

`device.error` is a terminal's own error report: `{code, message, origin, command_id, additional}`. `origin` is `cmd` when it is about a command this server sent.

Event types: `punch`, `user.updated`, `user.deleted`, `template.updated`, `template.deleted`, `device.registered`, `device.approved`, `device.rejected`, `device.removed`, `device.online`, `device.offline`, `device.oplog`, `device.error`, `device.clock_drift`, `device.storage_high`, `command.completed`, `upload.received`.

A punch marked as a repeat carries `duplicate: true` and `duplicate_of: <id>` in its payload. A late punch can change which of a group stands, so treat the mark as current only when you read it.

## Webhooks (admin)

| Call | What it does |
|---|---|
| `GET /api/webhooks` | List subscriptions. |
| `POST /api/webhooks` `{name, url, event_types?, sn?, secret?}` | Subscribe. The secret is shown once. |
| `PATCH /api/webhooks/:id`, `DELETE /api/webhooks/:id` | Change or remove one. |
| `POST /api/webhooks/:id/test` | Send a test event now. |
| `GET /api/webhooks/:id/deliveries`, `POST /api/webhooks/deliveries/:id/replay` | See deliveries, send one again. |

Each delivery is a POST of the event as JSON with `x-adms-timestamp` and `x-adms-signature: sha256=<hex>`, the HMAC-SHA256 of `<timestamp>.<body>` under the secret. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h and 12 h.

## Reports

`GET /api/reports/<name>?format=json|csv|xlsx|pdf` with `from=&to=` (or `month=YYYY-MM` for `monthly`), and optionally `sn=`, `department_id=`, `q=`. With no dates it covers today. A span may be at most 400 days. In CSV a PIN such as `007` loses its zeros when a spreadsheet opens it; the Excel file keeps them.

| Name | What it shows |
|---|---|
| `records` | Punch records. |
| `daily` | First in, last out, hours per person per day. |
| `monthly` | A month, person by day: hours, with `!` where a punch is missing. |
| `missing` | Days on which someone has an odd number of punches. |
| `uptime` | How much of each day each terminal was in contact, and the records it held. |
| `door` | Door-unlock signals sent from the server. |
| `failed` | Failed verifications and alarms the terminals reported. |

Late arrival, early leaving and overtime are not worked out here. They depend on each company's own rules and belong in the HR or payroll system, which can take `daily` or `records` and apply them.

## Terminals

| Call | What it does |
|---|---|
| `GET /api/devices?approval=accepted\|pending\|rejected\|all` | List terminals with status, address, what each holds, record memory, firmware. Accepted only unless asked. |
| `GET /api/devices/:sn` | One terminal. |
| `GET /api/connection` | What to type into a terminal to reach this server: `{address, source, https_port, http_port, alt_port}`. `alt_port` is the portal door, which also takes terminals over HTTPS, for networks where only 443 is open. `address` is null when the server was not told its own address; the portal then uses the name it was opened by. |
| `POST /api/devices` `{sn, name?, branch_id?}` (admin) | Add a terminal by serial before it connects, in a branch if given. It belongs to the organisation the request is about. `409 exists` when the serial is already a terminal here, in any organisation. |
| `POST /api/devices/:sn/approval` `{decision: "accept"\|"reject", organisation_id?}` (admin) | Let a waiting terminal in, or turn it away. A terminal nobody has seen before belongs to no organisation while it waits: accepting it puts it in `organisation_id` (a super admin may name one) or in the organisation the request is about. Without either, `400 organisation_required`. |
| `PATCH /api/devices/:sn` | Name, clock offset, protocol, schedule, pause, direct connection; `branch_id` (admin). |
| `DELETE /api/devices/:sn` (admin) | Remove it. Its punches are kept. |
| `POST /api/devices/:sn/sync` `{full?}` | Ask for anything not yet sent. |
| `POST /api/devices/:sn/resync` `{from, to}` or `{tables: [...]}` | Ask again for the records of those days, or for everything in the named tables. |
| `POST /api/devices/:sn/ops` `{op}` | `reboot`, `unlock`, `alarm_off`, `clear_log`, `clear_data`, `check`, `info`, `set_time`. |
| `POST /api/devices/:sn/copy` `{to, fingerprints?, faces?, cards?, people?}` | Copy its people to another terminal. |
| `GET /api/devices/:sn/backups`, `POST /api/devices/:sn/backups` | List snapshots, take one now. |
| `POST /api/devices/:sn/restore` `{backup_id?}` | Put a snapshot back, or everyone the directory knows on it. |
| `GET /api/backups/:id/download`, `DELETE /api/backups/:id` (admin) | Take a snapshot file away, or delete it. |
| `POST /api/devices/:sn/commands` `{text}` | Send a raw protocol command, one line. Commands that upgrade, run a shell, move files or set options are for an admin. |
| `GET /api/firmware`, `POST /api/firmware` `{name, data_b64}`, `DELETE /api/firmware/:id` (admin) | Firmware files. |
| `POST /api/devices/:sn/upgrade` `{file_id, confirm_sn, method?}` (admin) | Send a firmware file to a terminal. Not yet proven on real hardware. |
| `GET /api/discovery` (admin) | Address ranges worth scanning: the networks known terminals were seen on. |
| `POST /api/discovery/scan` `{range, port?, password?}` (admin) | Try every address in the range (a CIDR, one address, or `a.b.c.d-e`, at most 1024) on the terminals' management port, 4370 unless given. Units that answer come back with serial, name and firmware, and whether they are already known here; terminals already pushing from the range are listed too. Takes about ten seconds for a /24. |
| `POST /api/discovery/adopt` `{serial, host, port?, password?, name?}` (admin) | Take a found unit: add it by serial if new, let it in if it was waiting, store the address as its direct connection, and read it right away. |

## People

| Call | What it does |
|---|---|
| `GET /api/persons?q=&department_id=&status=&sn=` | The directory. |
| `GET /api/persons` | The directory. `sns=a,b` (on `/attendance`, `/attendance/present`, `/reports/:name` and `/users` too) narrows to a list of terminals, such as those of one branch. Each person carries `on: [{sn, fingerprints, faces, card, has_password}]`, what each terminal holds for them, never added up across terminals; with `sn`, that terminal alone. Filters: `q`, `branch_id`, `department_id`, `status`, `privilege` (role on the terminals: `0` user, `2` enroller, `6` admin, `14` super admin), `sn`. |
| `POST /api/persons`, `GET/PATCH /api/persons/:pin` | Add, read, change. |
| `DELETE /api/persons/:pin?records=1` | Delete the person outright: taken off every terminal that holds them, everything the server holds of them gone. With `records=1` their punches go too; without, the punches stay for the reports. Not for a caller limited to branches. |
| `POST /api/persons/delete` `{pins, records?}` | The same for many people at once. |
| `POST /api/org/clear` `{people?, records?, terminals?, structure?, confirm}` (admin) | Clear the organisation's data, in whole or in part. `confirm` must be the organisation's code. `structure` is branches, departments and time periods. Cannot be undone. |
| `POST /api/persons/:pin/transfer` `{to: [sn], with_templates?, temporary?: {start, end}}` | Put the person on terminals, with their credentials. `start` and `end` are `YYYY-MM-DD HH:MM:SS` in each terminal's own time. A terminal that reports `UserValidTimeFun=1` holds the dates itself (`temporary: "terminal"`); for the others the server puts the person on at the start (`via: "scheduled"`) and takes them off at the end (`temporary: "server"`). Everyone is taken off at the end. `ends_at` gives the end per terminal in UTC. Errors: `bad_window`, `already_ended`. |
| `POST /api/persons/:pin/remove` `{from: [sn]}` | Take them off terminals. |
| `POST /api/persons/:pin/disable` `{on?: [sn]}`, `.../enable` | Switch them off or on, on some terminals or all. Their credentials are kept. |
| `POST /api/persons/:pin/card` `{blocked}` | Block or unblock their card everywhere. |
| `POST /api/persons/:pin/enrol` `{sn, kind: "fingerprint"\|"face"}` | Ask a terminal to enrol them at its screen. |
| `POST /api/persons/:pin/periods` `{periods: [n]}` | Give the person up to three time periods, or none. Terminals refuse their punch outside them. |
| `GET /api/periods`, `PUT /api/periods/:number` `{name, days, note?}`, `DELETE /api/periods/:number` (changes: admin) | What each numbered period means. `days` is seven `{start, end}` entries, Sunday first. Saving a period sends its hours to every terminal on current firmware; each period in the list carries `terminals: [{sn, state}]`, where `state` is `set`, `waiting`, `outdated`, `failed` or `never`. |
| `POST /api/periods/:number/push` `{sns?: [...]}` (admin) | Send a period's hours again, to the terminals named or to all. For a terminal that was wiped, replaced, or refused the first time. |
| `POST /api/persons/:pin/photo` `{to: [sn], image_b64}` | Switched off unless the `photo_by_api` setting is on. Hands a JPEG to face terminals that take pictures. |
| `POST /api/persons/:pin/credentials/remove` `{sn, bio_type, idx}` | Remove one fingerprint, face or photo from one terminal. |
| `POST /api/persons/:pin/leave` | Mark as left and remove from every terminal. |
| `GET /api/persons.csv` | Export the directory. |
| `POST /api/persons/import` `{csv}` (admin) | Import people from a sheet. |
| `GET/POST /api/departments`, `PATCH/DELETE /api/departments/:id` | Departments: `{number, name, branch_id, parent_id}`. They form a tree inside one branch: a sub-department is always in its parent's branch (give only `parent_id` and it takes that branch), moving a department to another branch takes everything under it along, and deleting one moves what was under it up a level. Errors: `number_taken` (409), `loop`, `parent_in_other_branch`, `unknown_parent`, `unknown_branch` (400). |
| `POST /api/persons/assign` | Move many people at once: `{pins: [...], department_id}` or `{from_department_id, department_id}` (everyone directly in that department). `department_id: null` takes them out of any department. Answers `{moved, skipped}`. |
| `GET /api/org` | The whole structure in one reply: `{company: {name, address, phone}, today, branches: [{id, code, name, address, phone, note, people, in_now, terminal_list, department_list}], unassigned}`. A caller limited to branches gets only those. |

## Organisations (super admin)

| Call | What it does |
|---|---|
| `GET /api/organisations` | Every organisation on the server with counts of branches, terminals, people and accounts. |
| `POST /api/organisations` `{code, name, address?, phone?, note?}` | Add one. `409 code_taken` when the code is in use. |
| `GET /api/organisations/:id`, `PATCH /:id`, `DELETE /:id` | One organisation. Only an empty one can be deleted: `409 organisation_not_empty` while it still holds terminals, people, branches or accounts. |

## The organisation at hand, settings, access (changes: admin)

| Call | What it does |
|---|---|
| `GET /api/org` | The organisation the request is about as one picture: its details, each branch with its terminals and departments, who is in. |
| `PATCH /api/org` `{name?, address?, phone?}` | The organisation's own details, as its admin may change them. Shown beside the logo and on exported reports. |
| `GET /api/branches`, `POST`, `PATCH /:id`, `DELETE /:id` | Branches: `{code, name, address, phone, note}`. Changing them needs the admin role. |
| `GET /api/settings`, `PATCH /api/settings` | Per organisation: warning thresholds, repeat window, backups kept, pictures by API. Server-wide, a super admin only (`403 platform_required` otherwise): approval of new terminals, allowed web origins. |
| `GET/POST /api/auth/access/accounts`, `PATCH/DELETE /:id` | Portal accounts, their role and branches. A new account belongs to the organisation the request is about; a super admin may pass `org_id` (an id, or `null` for a super admin, which must have the `admin` role). An admin of an organisation sees and changes only its accounts; the last active admin at each level is protected (`409 last_admin`). |
| `GET/POST /api/auth/access/tokens`, `PATCH /:id` `{manual_punch}`, `DELETE /:id` | API tokens: role, branches, allowed addresses, and whether the token may add hand-entered records. The organisation rule is the same as for accounts. |
| `GET /api/auth/me` | Who is calling: `{name, role, kind, branches, platform, org_id, organisation}`. `platform` is true for a super admin; `organisation` is the one the caller belongs to, with its id, code and name, or null. |

## Logs

| Call | What it shows |
|---|---|
| `GET /api/logs/audit` | Who changed what, from where. Not for branch-limited callers. |
| `GET /api/logs/commands` | Commands sent to terminals and their results. `reason` says in words why one failed: the terminal's refusal code, or a later error report from the terminal about that command (for example "no face was found in the photo"). A command the terminal first accepted and then reported failing is marked `failed`. |
| `GET /api/logs/uploads` | What terminals uploaded and whether it was read. |
| `GET /api/logs/oplog?op=` | What was done at the terminals themselves. |
| `GET /api/logs/errors?source=` | What went wrong. |
| `GET /api/status/uploads` | How many saved uploads are waiting to be sorted. |

### Filtering by branch and department

`GET /api/persons`, `GET /api/attendance`, `GET /api/attendance/present` and every report take `branch_id` and `department_id`. A department includes all its sub-departments. `branch_id` selects people whose department is in that branch.
