On this page0
Unfamiliar word? See Glossary: words used in these docs. Looking for the tasks in order? Begin at Start Here.
Reference
GDPR Compliance
What your instance provides for the General Data Protection Regulation (GDPR): giving a person a copy of their data, erasing it, recording their acceptance of your terms, and protecting it in storage.
Privacy By Design
Because your instance runs on a dedicated server provisioned for you alone, your candidates’ personal data, employee files and cloud map stay on that server, isolated from every other customer and never pooled onto servers shared between customers.
Data Privacy & Control
GDPR sets rules for collecting, using and erasing personal data. These are the parts of Zonitix you use to follow them:
Right To Erasure (Forgotten)
Right Of Access (DSAR)
Data Minimization
Retention: Automatic For Candidates, By Hand For Employees
Terms Acceptance (GDPR Article 7)
Zonitix records each person’s acceptance of your terms and privacy policy, and asks for it at sign-in:
Retained Consent Timestamps
Block Unconsented Access
If something here is not working as you expect, start with When something goes wrong.
The plain explanation is above. This part is for IT and developers: the exact settings, permission names, API addresses and data fields behind it. Closed: select the heading above to open it. While it is closed, your browser’s find-on-page (Ctrl+F or Cmd+F) does not search this part.
How A Data Request And An Erasure Run
The steps on screen are in the guide Handle a personal-data request. This section gives the endpoints behind them and what each one returns.
GDPR Articles 15 (access) and 17 (erasure) are served by concrete endpoints on your instance, reachable through the gateway under /api/v1/user-management. All are audited. None of them runs on a schedule — employee erasure is an operator action, not a background purge (see the invariants at the end). Candidate records are the exception: a separate recruitment-side retention sweep can purge non-hired candidates automatically once a retention period is set.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/v1/user-management/users/me/export | Self-service DSAR: exports the CALLER’s own profile, group & role memberships, permissions and up to 200 recent audit entries as JSON | Any authenticated session (own record only) |
| GET | /api/v1/user-management/users/:id/export | Admin DSAR: the same export for ANOTHER user by id, including former / offboarded employees who can no longer sign in | manage_users |
| DELETE | /api/v1/user-management/users/:id | Right-to-erasure for an offboarded account: deletes the row (204), or anonymizes it when history still references it (200) | manage_users |
| GET | /api/v1/user-management/devices/owner?userId= | Device side of an access request: every device the person owns, with the owner details held on it | manage_devices or manage_security |
| DELETE | /api/v1/user-management/devices/owner?userId= | Device side of erasure: clears the signed-in user name, hostname and last check-in IP and unassigns the owner on each device the person owned. The asset record is kept. Audited as DEVICE_OWNER_ERASED; returns { erased: n } | manage_devices or manage_security |
| GET | /api/v1/user-management/users/org-tree?format=csv | Admin/bulk directory export (Name, Email, Title, Department, Manager) as CSV | view_org_chart or view_dashboard; the CSV also needs view_users or manage_users (403 without) |
Right Of Access — Self-Service Export
GET /users/me/export is gated only by having a valid session — there is no :id parameter to point at someone else. The handler reads req.user.id from the trusted x-user context the gateway injects, so it can only ever return the caller’s own record. The response embeds the consent record: acceptedTerms / acceptedTermsAt travel inside profile.attributes. Every call writes a PEOPLE_DATA_EXPORTED audit entry (kind: "dsar_self").
// Pull your own subject-access export (auth is your session; the
// gateway adds the internal service token + x-user downstream):
curl -s https://<your-instance>-api.zonitix.com/api/v1/user-management/users/me/export \
-H 'Cookie: <your authenticated session>'
// 200 OK — the shape the controller returns:
{
"exportedAt": "2026-08-09T22:20:00.000Z",
"requestingUser": "you@company.cc",
"profile": {
"id": 10, "firstName": "...", "lastName": "...", "email": "you@company.cc",
"username": "...", "status": "active", "department": "...",
"mfaEnabled": true,
"attributes": { "acceptedTerms": true, "acceptedTermsAt": "2026-06-28T22:20:00Z" },
"createdAt": "...", "updatedAt": "..."
},
"groups": [ { "id": 1, "name": "Everyone", "description": "..." } ],
"roles": [ { "id": 2, "name": "standard_user", "description": "..." } ],
"permissions": [ "..." ],
"recentAuditLogs": [ { "action": "...", "details": "...", "level": "...", "createdAt": "..." } ],
"crossServiceData": { "signatures": ..., "devices": ..., "...": ..., "notes": [ ] }
}
Scope to check as an inspector: the controller assembles the response from an explicit field allowlist, so it carries no password hash, no MFA secret and no recovery codes — those columns are never selected into the payload. recentAuditLogs is filtered to rows whose userEmail equals the caller’s, newest first, capped at 200. A missing profile row returns 404.
crossServiceData is what the other services on your instance hold about the person: e-signature records, devices they own, onboarding / offboarding task data, compliance assignments, the candidate record they were hired from, finance directory and budget-comment entries, and SaaS-usage observations. It is gathered best-effort: a store that could not be reached is named in notes rather than silently left out, so check that list is empty before you treat an export as complete.
Right Of Access — Admin Export For Someone Who Cannot Sign In
A terminated or offboarded employee cannot authenticate to run the self-service export. GET /users/:id/export (manage_users) is the admin-on-behalf path: it loads the subject by id whatever their status, returns the same structure including crossServiceData, and reports the subject’s permissions, not the admin’s. It writes PEOPLE_DATA_EXPORTED at warning level with kind: "dsar_admin" and the acting admin, so it is never confused with a self-service export in an access review.
Right Of Access — Candidate Export
Candidates have no login. Recruitment staff produce a full per-subject export from GET /candidates/:id/data-export (manage_recruitment): profile, contact details, interviews, offer terms, the message thread and the signed offer document, as one audited JSON file.
Right To Erasure — Delete, Or Anonymize
Erasure of an employee account is DELETE /users/:id, gated by the manage_users permission. The account must already be offboarded: an account whose status is still active is refused with 409, because offboarding is what revokes access and deprovisions connected accounts. There are then two successful outcomes. Where nothing else references the row, it is genuinely deleted (the User model is not a soft-delete model, so this is a real SQL DELETE) and the call returns 204 No Content with a USER_DELETED audit entry. Where history that must be kept still references the account (audit records, tasks, approvals), the row cannot be deleted, so it is anonymized instead: name, email, phone, employee id, photo, position, department and attributes are overwritten or cleared, roles and groups are removed, sign-in is disabled, and the status becomes deleted. That returns 200 with { "anonymized": true, "crossServiceErasure": ... } and a USER_ANONYMIZED audit entry. The erasure’s own audit entry records a keyed pseudonym for the subject, not their email.
// Erase user #42 (admin session carrying manage_users):
curl -s -i -X DELETE \
https://<your-instance>-api.zonitix.com/api/v1/user-management/users/42 \
-H 'Cookie: <admin session with manage_users>'
// A cookie-authenticated DELETE is state-changing, so the gateway’s
// Origin check (CSRF guard) applies — send it from an allowed origin.
HTTP/1.1 204 No Content // erased: row deleted
HTTP/1.1 200 OK // erased: row kept, personal data anonymized
HTTP/1.1 409 Conflict // account still active (offboard first), or the support account
HTTP/1.1 403 Forbidden // target is the primary administrator
HTTP/1.1 404 Not Found // no such user id
Primary Admin Cannot Be Erased
isPrimaryAdmin is set. The break-glass account is deliberately non-erasable through this route.What The Erasure Reaches, And What It Keeps
USER_ERASURE_RESIDUAL warning names it — check for that entry. The audit log itself is retained as the compliance record and is not scrubbed. The Zonitix support account is managed only through the support-access grant and cannot be deleted here (409).No Automated Retention Job
Be precise with your DPO about the split. Employee account records are never deleted on a timer — erasure is an operator running DELETE /users/:id, enforced by process. Candidate/applicant records can expire automatically: set a retention period in months (off by default) and a scheduled sweep permanently purges any non-hired candidate untouched beyond that window, roughly every six hours, auditing each purge. So candidate minimization can be software-enforced; employee erasure stays a deliberate operator action.
Erasure Is Irreversible — Export First
Neither outcome can be undone from the application: a deleted row is gone and an anonymized one has had its personal fields overwritten. If a subject requests both a copy of their data and erasure, take the export before the delete — /users/:id/export as an admin, or /users/me/export by the subject while they can still sign in. Afterwards the profile fields are unrecoverable short of a database restore.
Export All Company Data
Separate from per-person exports, an administrator can take a copy of everything on the instance from Security Settings → Data export. It is available on every plan. The export is built in the background as a ZIP, held encrypted on your instance, and expires after 24 hours. To download it you mint a link that works once, for 15 minutes, and only for the administrator who minted it.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/v1/compliance/data-export/jobs | List export jobs and the limits in force. body No query parameters. returns 200 { jobs: TenantExportJob[], limits: { maxPerDay: 3, artifactTtlHours: 24, linkTtlMinutes: 15 } } — TenantExportJob is typed under Response Objects; newest first, at most 20 | Session + administrator role |
| GET | /api/v1/compliance/data-export/jobs/:id | One job’s progress. body No query parameters. :id is a whole number. returns 200 a TenantExportJob | 404 { error:'Export not found', code:null } | Session + administrator role |
| POST | /api/v1/compliance/data-export/jobs | Start an export (202, runs in the background). One at a time; at most 3 in 24 hours (429 beyond that). body No body. returns 202 a TenantExportJob, newly created with status queued | 409 code EXPORT_IN_PROGRESS when one is queued or running | 429 code EXPORT_RATE_LIMITED after 3 in 24 hours | 503 code NO_ENCRYPTION_KEY when the instance has no key to encrypt the file with guard One at a time. Failed jobs do not count toward the 3 in 24 hours. Audited as TENANT_EXPORT_REQUESTED, then TENANT_EXPORT_READY or TENANT_EXPORT_FAILED. Not safe to repeat blindly: each accepted call builds another export. | Session + administrator role + sudo |
| POST | /api/v1/compliance/data-export/jobs/:id/link | Mint the single-use, 15-minute download link. body No body. returns 200 { token: string (64 hex characters), linkExpiresAt: string (date-time), path: string (/compliance/data-export/download/<token>) } | 404 'Export not found' | 410 code EXPORT_EXPIRED when the job is not ready or its file has expired guard The link lasts 15 minutes, or until the file expires if that is sooner, works once, and only for the administrator who minted it. Minting again replaces the previous link. Audited as TENANT_EXPORT_LINK_ISSUED. | Session + administrator role + sudo |
| GET | /api/v1/compliance/data-export/download/:token | Stream the ZIP and burn the link. It works only for the administrator who minted it. body No query parameters. :token is the 64 hex characters from the link call. returns 200 the ZIP (application/zip, Content-Disposition attachment, Content-Length, X-Content-SHA256 = the SHA-256 of the ZIP, Cache-Control no-store) | 410 code LINK_USED_OR_EXPIRED when the link was already used, has expired or was minted by someone else | 410 code EXPORT_EXPIRED when the file is no longer on the server guard Single use: a second call with the same token answers 410. Audited as TENANT_EXPORT_DOWNLOADED; a refused call as TENANT_EXPORT_DOWNLOAD_DENIED. If the stream is cut short the download is incomplete: compare the file's SHA-256 with the header. | Session + administrator role |
This is gated on the administrator role itself rather than a permission, so it cannot be delegated by granting a permission to another role. Starting an export, minting a link and every download are written to the audit log.
Encryption & Storage Security
Zonitix implements technical safeguards under GDPR Article 32. To be precise about scope: the transparent field-level AES-256-GCM encryption below covers authentication secrets and integration credentials, plus the highest-sensitivity recruitment fields specifically — an offer’s salary and a candidate’s phone number — which are encrypted at rest in dedicated cipher columns. Candidate and employee records are not column-encrypted wholesale (a candidate’s email, for instance, stays in clear so account matching still works); those rely on access control and single-tenant isolation:
// Transparent AES-256-GCM encryption is applied at the ORM layer:
const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
let encrypted = cipher.update(sensitiveData, 'utf8', 'hex');
encrypted += cipher.final('hex');
// Resumes and PII media assets are isolated in secure storage blocks:
- CVs are stored on your instance’s own disk, outside the web root. Object-storage upload is stubbed, not implemented — nothing is written to S3 today.
- Resumes are served through a permission-gated download route with path-traversal hardening. There are no public or pre-signed URLs — a resume is never reachable without an authenticated, authorised request.
Technical Consent Tracking Flow
When a user logs in, the authentication controller checks for the presence of a consent signature and stores the verification. The check runs in the identity service during login, not at the gateway. Accepting writes acceptedTerms, acceptedTermsAt and acceptedTermsVersion to the user’s profile attributes and raises a TERMS_ACCEPTED audit entry; no user-facing endpoint edits these fields. Without a session, no downstream endpoint is reachable:
// API payload to submit and record terms consent:
POST /api/v1/auth/login
Payload: { "username": "admin@company.cc", "password": "S3cur3P@ss!", "consentAccepted": true }
// Recorded on the user row's attributes JSON:
attributes: {
acceptedTerms: true,
acceptedTermsAt: "2026-06-28T22:20:00Z",
acceptedTermsVersion: "<version of the terms accepted>"
}
// Without consent, login returns a challenge rather than a session:
Response: { "termsAcceptanceRequired": true, "userId": 10, "termsVersion": "<current version>" }
// A sign-in is let through only when acceptedTermsVersion equals the current version.
// Passkey and emailed-link sign-ins get the same challenge with a termsToken,
// exchanged at POST /api/v1/auth/accept-terms (see SSO, Passwordless Login & SCIM).