# Zentrik API v1

Base: `/api/v1/t/{company-slug}`. Use `Authorization: Bearer YOUR_TOKEN` and `Accept: application/json`. Create/revoke tokens from Company settings > API access after password confirmation. Tokens are bound to the issuing user and expire within 90 days; current role/module checks still apply. MFA enrollment is optional for the issuing account. The token bucket allows 120 calls per minute. Request bodies are limited to 256 KiB.

## Read resources

`GET /resources/{resource}` returns a Laravel pagination envelope with 50 records per page. Pass `?page=2` for subsequent pages. Resources: organization, positions, employees, documents, recruitment, career, attendance, leave, payroll, requests, assets, fleet and workflows. Required ability is `{resource}:read`, plus the corresponding current role permission and module. Only explicit registered columns are serialized. Document content, bank details, token hashes, integration secrets, decrypted files and custom form answers are excluded. Candidate documents are not exposed through the generic document register. Private employee-service workflows are excluded from the generic workflow resource and module history; use the authorized employee-service screens for those requests.

## Business commands

`POST /commands/{command}` accepts the existing typed domain service inputs. Send a unique UUID `Idempotency-Key`. A successful command returns `{ "ok": true, "reference": "command.name", "id": 123 }`. Repeating an identical parsed body/key/token returns the committed result without a second mutation. Reusing that key with a different command/body returns HTTP 409. The receipt commits in the same transaction as the business change.

| Commands | Ability |
| --- | --- |
| organization.unit, organization.position | organization:write |
| employees.save | employees:write |
| recruitment.vacancy | recruitment:write |
| career.create, career.decide | career:write |
| attendance.record | attendance:write |
| leave.request, leave.decide | leave:write |
| requests.create, requests.decide | requests:write |
| assets.category, assets.create, assets.issue, assets.return | assets:write |
| fleet.create, fleet.licence, fleet.issue, fleet.return | fleet:write |
| payroll.create, payroll.transition | payroll:write |

Create/configuration commands use the same named fields and validators as their corresponding company screens. Organization creation requires the owner's current `organization.manage` permission; employee creation requires `employees.manage`. A token write ability does not replace these permissions, including on retries after a role change. Organization input is allowlisted and cannot set record or tenant ownership IDs. Decision/return commands use integer `id` plus `decision` (approved/rejected) and `note`, or `evidence` for equipment returns. Career decisions also accept subject_type record/vacancy/offer; the typed service still requires the relevant recruitment or lifecycle permissions. Payroll create takes `period` YYYY-MM; transitions take id/action/note, with action submit/approve/return/release. Equipment issue takes the asset id and employee_id/evidence. Asset scope cannot issue/return a vehicle through the shared equipment record.

Approval separation, verified licences, live custody, payroll source hashes, period locks and tenant checks remain enforced by domain services. There is no unrestricted model CRUD endpoint. Encrypted file operations and bank exports continue through their authenticated UI routes.

## Webhooks

Configure events from the central registry. JSON contains id, type, subject and occurred_at. Headers: X-Zentrik-Event, X-Zentrik-Timestamp, X-Zentrik-Signature. Verify the exact raw body with `HMAC-SHA256(secret, timestamp + "." + body)` and compare to the hex digest after `v1=` in constant time. Reject stale timestamps (for example outside a five-minute window), deduplicate event IDs and return 2xx only after recording acceptance.

Delivery is at least once: a process failure after the receiver commits may cause a retry with the same event ID. Failed sends back off, stop after five attempts and show sanitized diagnostics. Receiver response contents are never saved. DNS/HTTPS restrictions are applied again on every attempt. No scheduler or delivery worker was started on the working local setup.

Local audit extension: `employees.save` accepts an optional `custom_values` object keyed by active company field keys. Server-defined type/choice/required rules apply; unknown/inactive keys are rejected. These values remain encrypted and excluded from the public employee read registry. Configure definitions through the authorized Settings screen.
