Reports
The Reports API returns analytics and compliance data directly - there's no job queue, no polling, and no report IDs to fetch later. Every endpoint here runs its query and responds with the data (or a PDF/CSV file) in the same request.
Everything lives under /api/reports and uses your dashboard session rather than an API key.
- Analytics endpoints need
analytics_view(Super Admin, Admin, HR and Manager by default; custom roles can change it). The downloads (/analytics-pdf,/sla-compliance/export) also needanalytics_export(Super Admin, Admin and HR by default). /my-tasksand/upcoming-exitsare open to any signed-in user (they feed Mission Control);/compliance-summaryneedscompliance_view;/offboarding/:id/pdfneedsoffboardings_view.- Every endpoint only counts the offboardings the caller can see: a user limited to some departments, or to their direct reports, gets figures for those only.
- Dates are
YYYY-MM-DD;fromandtogo together and are both inclusive. - Durations are from creation to completion (
completed_at, set when the status becomescompleted), for completed offboardings only.
If you need scheduled, recurring delivery instead of querying on demand, see Scheduled reports below - that part genuinely is a job that runs on a schedule and emails or webhooks the result out.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/reports/summary | Offboarding counts, stage/type breakdowns, completion trend |
GET | /api/reports/upcoming-exits | Employees exiting within N days |
GET | /api/reports/my-tasks | The current user's outstanding checklist tasks |
GET | /api/reports/compliance-summary | NDA / exit-interview / legal-hold counts |
GET | /api/reports/offboarding/:id/pdf | Single-employee offboarding PDF |
GET | /api/reports/advanced | Velocity trend, bottleneck tasks, time-by-department |
GET | /api/reports/benchmarks | Org metrics vs. other organisations and typical values |
GET | /api/reports/analytics-pdf | PDF export of the analytics dashboard |
GET | /api/reports/sla-compliance | SLA compliance rates, breach trend, worst offenders |
GET | /api/reports/sla-compliance/export | CSV of every SLA-tracked checklist item |
Summary
GET /api/reports/summary?from=2025-01-01&to=2025-06-30
from/to are optional but must be supplied together; omitting them returns all-time figures. The range is at most 10 years.
{
"range": { "from": "2025-01-01", "to": "2025-06-30" },
"stageCounts": [{ "status": "notice_period", "cnt": "4" }],
"sepTypeCounts": [{ "separation_type": "resignation", "cnt": "30" }],
"completedByMonth": [{ "month": "2025-06", "cnt": "8" }],
"avgCompletionDays": 6.4,
"completedCount": 21,
"overdueCount": 3,
"topServices": [{ "service": "slack", "cnt": "40" }],
"checklistStats": { "total": 512, "done": 470 },
"deptBreakdown": [{ "department": "Engineering", "cnt": "18" }],
"licenseReclamation": {
"licensesThisMonth": 2, "licensesThisYear": 9, "costThisMonth": 19.98, "costThisYear": 89.91, "currency": "USD",
"byCurrency": [{ "currency": "USD", "licensesThisMonth": 2, "costThisMonth": 19.98, "licensesThisYear": 9, "costThisYear": 89.91 }]
}
}| Field | Covers |
|---|---|
stageCounts | Open offboardings right now, by stage (no completed). Not affected by the range. |
completedByMonth, avgCompletionDays, completedCount | Offboardings completed in the range. month is YYYY-MM, oldest first; months with none are left out. avgCompletionDays is null when none completed. |
sepTypeCounts, deptBreakdown, checklistStats | Offboardings created in the range. |
topServices | Access removed in the range (revoked or manual_confirmed). |
overdueCount | Open offboardings past their exit date, right now. |
licenseReclamation | This month / this year, one entry per currency in byCurrency; the top-level cost fields are the currency with the largest savings this year. |
Upcoming exits
GET /api/reports/upcoming-exits?days=14
days is 1 to 60 (default 14; anything that isn't a number uses the default). Returns an array, one row per employee exiting within the window, ordered by exit date:
[
{
"id": "8f4c6d1a-...",
"employee_name": "Jordan Lee",
"department": "Engineering",
"job_title": "Senior Engineer",
"status": "notice_period",
"last_working_day": "2025-07-01",
"days_remaining": 5,
"pending_tasks": 3
}
]My tasks
GET /api/reports/my-tasks
Returns up to 50 outstanding checklist items on open offboardings within the caller's scope. For most roles the items are matched by role against each item's assigned_to field (plus items assigned to all); Super Admins and Admins see every open task instead, since nothing is ever assigned to those role names.
Compliance summary
GET /api/reports/compliance-summary
Counts over the offboardings the caller can see:
{
"total_offboardings": "12",
"nda_signed": "9",
"nda_pending": "3",
"exit_interview_done": "7",
"legal_hold_count": "1",
"closed_without_nda": "2",
"closed_with_pending_documents": "1"
}| Field | Covers |
|---|---|
total_offboardings, nda_signed, nda_pending, exit_interview_done | Active offboardings (not completed or cancelled) |
legal_hold_count | Offboardings on legal hold, of any status |
closed_without_nda | Completed offboardings with no signed NDA |
closed_with_pending_documents | Completed offboardings that still have a compliance document in pending |
Single-employee PDF
GET /api/reports/offboarding/:id/pdf
Requires the pdfExport plan feature. Returns a PDF covering employee details, checklist status, access-revocation status, compliance documents, and any org-defined custom fields with a value set. Returns 404 if the offboarding doesn't exist in your org or is outside the caller's department scope. Each download is audited (offboarding.pdf_exported) and is tamper-evident; see PDF integrity.
Advanced analytics
GET /api/reports/advanced?from=&to=
velocityTrend has one row per month of the range (the last 12 months without one), empty months included: started counts offboardings created in the month, completed those completed in it. timeByDept and avgBySepType cover offboardings completed in the range. bottleneckTasks is right now.
{
"velocityTrend": [{ "month": "2025-06", "started": "10", "completed": "8" }],
"timeByDept": [{ "department": "Engineering", "avg_days": "7.2", "cnt": "6" }],
"bottleneckTasks": [{ "task": "Revoke AWS access", "total": "12", "pending": "5", "pending_pct": "42" }],
"avgBySepType": [{ "separation_type": "resignation", "avg_days": "6.1", "cnt": "20" }]
}bottleneckTasks only includes tasks that appear at least twice across active offboardings, ranked by the percentage still incomplete.
Benchmarks
GET /api/reports/benchmarks
Your offboardings created in the last 12 months (within your scope), compared with:
platform: the same figures across other organisations on this instance. Only when at least 5 other organisations and 50 offboardings contribute (sufficientSample: true); otherwise every value isnull. The caller's org is never included,totalOffboardingsis rounded down to a multiple of 10, andplatformSepDistgives shares only. Cached for an hour per org.industry: fixed typical values (not a survey), kept under this key for compatibility.
{
"org": { "avgCompletionDays": 6.4, "total": 42, "completionRate": 88, "onTimeRate": 91, "overdueCount": 2 },
"platform": { "sufficientSample": true, "avgCompletionDays": 7.1, "totalOffboardings": 8400, "completionRate": 81, "onTimeRate": 76 },
"industry": { "avgCompletionDays": 8, "completionRate": 80, "onTimeRate": 65 },
"minPlatformOrgs": 5,
"scoped": false,
"orgSepTrend": [{ "month": "2025-06", "separation_type": "resignation", "cnt": "4" }],
"platformSepDist": [{ "separation_type": "resignation", "pct": 53.6 }],
"orgDeptDuration": [{ "department": "Engineering", "avg_days": "7.2", "cnt": "6" }]
}onTimeRate is the share of completed offboardings completed on or before their exit date. scoped is true when the caller only sees some departments.
Analytics PDF
GET /api/reports/analytics-pdf?from=&to=
Requires pdfExport and analytics_export. KPIs (started and completed in the range, average duration, overdue now), separation types, duration by department and bottleneck tasks, with the same definitions as /summary. Audited (analytics.exported) and tamper-evident; see PDF integrity.
SLA compliance
GET /api/reports/sla-compliance?period=week&from=&to=
Requires the slaComplianceDashboard plan feature. period is week or month (default week); from/to default to the last 90 days, are both inclusive, and can't be more than 2 years apart. Every figure covers SLA tasks whose deadline falls in the range.
breachTrend has one entry per week or month of the range (empty ones included), with tasks grouped by deadline: period is the bucket's first day (YYYY-MM-DD), and rate is on-time / completed for that bucket (0 to 100, null when nothing due then was completed).
{
"period": "week",
"from": "2025-04-01",
"to": "2025-06-30",
"totalSlaTracked": 340,
"completedCount": 300,
"onTime": 270,
"breachedTotal": 30,
"overallRate": 90.0,
"byDepartment": [{ "dept": "Engineering", "total": 80, "completed": 72, "onTime": 65, "breached": 7, "rate": 90.3 }],
"byAssignee": [{ "assignee": "it-team", "total": 120, "completed": 110, "onTime": 100, "breached": 10, "rate": 90.9 }],
"breachTrend": [{ "period": "2025-06-02", "breachCount": 2, "total": 24, "completed": 24, "onTime": 22, "rate": 91.7 }],
"worstItems": [{ "task": "Revoke VPN access", "totalSla": 40, "breaches": 8, "avgSlaHours": 24.5, "breachRate": 20.0 }]
}A checklist item only counts toward these numbers if it has an SLA deadline set - items without one are excluded entirely, not counted as compliant.
SLA compliance export
GET /api/reports/sla-compliance/export?from=&to=
Requires slaComplianceDashboard and analytics_export. Same range, defaults and scope as /sla-compliance. Returns a CSV with one row per SLA task: task, employee name/email/department, assignee, SLA hours, SLA deadline, completed, breached, completed-at, and hours overdue. Values that could be interpreted as spreadsheet formulas are neutralised. At most 50,000 rows (X-Rows-Truncated is set when there were more). Audited (analytics.exported).
Scheduled reports
Recurring report delivery is a separate, genuinely job-based feature under /api/scheduled-reports (requires the scheduledReports plan feature). Unlike everything above, this one really does run asynchronously - on the schedule you configure, not synchronously in the request.
| Method | Path | Description |
|---|---|---|
GET | /api/scheduled-reports | List an org's scheduled reports |
POST | /api/scheduled-reports | Create one |
PATCH | /api/scheduled-reports/:id | Update it |
DELETE | /api/scheduled-reports/:id | Delete it |
GET / POST / PATCH / DELETE | /api/scheduled-reports/distribution-lists[/:id] | Manage reusable recipient lists |
Every route here, the GETs included, needs the settings_manage permission (Super Admin and Admin by default, or a custom role that grants it); anyone else gets 403.
Creating a schedule
{
"name": "Weekly HR digest",
"type": "weekly_summary",
"schedule": "weekly",
"columns": ["employee_name", "department", "status", "exit_date"],
"grouping": "department",
"date_range": "last_30_days",
"recipients": ["hr-team@acme.com"],
"include_pdf": true
}type is one of weekly_summary, monthly_summary, offboarding_stats, overdue_tasks, or custom. schedule is daily, weekly (Monday) or monthly (the 1st), in UTC. You need at least one of recipients (up to 50 emails) or a distribution_list_id from your org.
filters:{ "status": [...], "department": [...], "separation_type": [...] }, all optional; the summary and the rows both follow them.grouping:none,department,separation_type,status, orassignee(only foroverdue_tasks).last_month/last_quarterare the previous calendar month / quarter (UTC).PATCHchanges only the fields you send.- A member whose access is limited to some departments can only create or edit reports whose
departmentfilter stays within those departments (403otherwise), and members limited to their direct reports can't create or edit reports at all. The report runs org-wide and is emailed to whoever its author picks, so the limit lives in the report's own filter. - Recipients who aren't users in the org get totals only in the email (and PDF): the summary counts without the legal hold count, or for
overdue_tasksthe number of overdue tasks. No per-person rows, so no names, emails, roles, departments, separation types, dates, legal hold or assignees. delivery_webhookis never returned in full: responses givehttps://<host>/****. Sending that masked value back in aPATCHkeeps the stored URL; any other value replaces it (after the checks below), andnullor""removes it.DELETE /distribution-lists/:idreturns{ "success": true, "affectedReports": [{ "id", "name", "noRecipients" }] }: the reports that used the list (they lose it), and whether each is left with nobody to send to.404if the list doesn't exist.delivery_webhookmust be HTTPS and resolve to a public address, checked when saved and again before each send; redirects aren't followed. It's a plain unsigned POST ({ report_id, report_name, org_name, generated_at, date_range, summary, row_count }), not the signed mechanism of Webhooks; there's noX-PurpletGo-Signatureheader.GETreturnslast_run(last delivery),last_attempt_atandlast_error. If no email could be sent, the next hourly run tries again.- Changes and each delivery are audited (
scheduled_report.*,distribution_list.*).
PDF integrity
Every PDF these endpoints produce, including the ones attached to scheduled-report emails, can be proved unaltered later:
- Each PDF has a random document ID, printed in its footer and stored in its metadata (
Keywords: purpletgo-document-id:<id>). - The SHA-256 of the exact file and the document ID are written into the audit entry for the download (
offboarding.pdf_exported,analytics.exported) or the send (scheduled_report.sent, one per copy). That entry is part of the audit log's hash chain. - When the deployment has a signing key, the file is also signed with Ed25519. Downloads carry the signature in
X-Signature-Ed25519(withX-Signing-Key-Id); scheduled-report emails attach it as<file>.pdf.sig. Verify it with the public key fromGET /api/audit-log/signing-key, as described there. - Downloads also carry
X-Document-IdandX-Content-SHA256. - A download whose audit entry can't be written is refused with
503rather than sent unrecorded.
To check a copy someone sends you: read the document ID from its footer, find the matching audit entry, and compare sha256sum of the file with the hash recorded there.
Error responses
These endpoints return plain { "error": "..." } bodies rather than a fixed error-code enum:
| Status | When |
|---|---|
400 | Invalid from/to (not a real date, only one given, from after to, too long a range) |
403 | Missing analytics_view / analytics_export (the body has permission), or the plan doesn't include a gated feature (pdfExport, slaComplianceDashboard; the body has upgradeRequired) |
404 | Offboarding not found or outside your scope (PDF export) |
500 | Query or PDF generation failed |
503 | The PDF could not be recorded in the audit log, so it was not sent |