API Reference
Reports

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 need analytics_export (Super Admin, Admin and HR by default).
  • /my-tasks and /upcoming-exits are open to any signed-in user (they feed Mission Control); /compliance-summary needs compliance_view; /offboarding/:id/pdf needs offboardings_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; from and to go together and are both inclusive.
  • Durations are from creation to completion (completed_at, set when the status becomes completed), 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

MethodPathDescription
GET/api/reports/summaryOffboarding counts, stage/type breakdowns, completion trend
GET/api/reports/upcoming-exitsEmployees exiting within N days
GET/api/reports/my-tasksThe current user's outstanding checklist tasks
GET/api/reports/compliance-summaryNDA / exit-interview / legal-hold counts
GET/api/reports/offboarding/:id/pdfSingle-employee offboarding PDF
GET/api/reports/advancedVelocity trend, bottleneck tasks, time-by-department
GET/api/reports/benchmarksOrg metrics vs. other organisations and typical values
GET/api/reports/analytics-pdfPDF export of the analytics dashboard
GET/api/reports/sla-complianceSLA compliance rates, breach trend, worst offenders
GET/api/reports/sla-compliance/exportCSV 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 }]
  }
}
FieldCovers
stageCountsOpen offboardings right now, by stage (no completed). Not affected by the range.
completedByMonth, avgCompletionDays, completedCountOffboardings completed in the range. month is YYYY-MM, oldest first; months with none are left out. avgCompletionDays is null when none completed.
sepTypeCounts, deptBreakdown, checklistStatsOffboardings created in the range.
topServicesAccess removed in the range (revoked or manual_confirmed).
overdueCountOpen offboardings past their exit date, right now.
licenseReclamationThis 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"
}
FieldCovers
total_offboardings, nda_signed, nda_pending, exit_interview_doneActive offboardings (not completed or cancelled)
legal_hold_countOffboardings on legal hold, of any status
closed_without_ndaCompleted offboardings with no signed NDA
closed_with_pending_documentsCompleted 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 is null. The caller's org is never included, totalOffboardings is rounded down to a multiple of 10, and platformSepDist gives 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.

MethodPathDescription
GET/api/scheduled-reportsList an org's scheduled reports
POST/api/scheduled-reportsCreate one
PATCH/api/scheduled-reports/:idUpdate it
DELETE/api/scheduled-reports/:idDelete 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, or assignee (only for overdue_tasks).
  • last_month / last_quarter are the previous calendar month / quarter (UTC).
  • PATCH changes only the fields you send.
  • A member whose access is limited to some departments can only create or edit reports whose department filter stays within those departments (403 otherwise), 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_tasks the number of overdue tasks. No per-person rows, so no names, emails, roles, departments, separation types, dates, legal hold or assignees.
  • delivery_webhook is never returned in full: responses give https://<host>/****. Sending that masked value back in a PATCH keeps the stored URL; any other value replaces it (after the checks below), and null or "" removes it.
  • DELETE /distribution-lists/:id returns { "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. 404 if the list doesn't exist.
  • delivery_webhook must 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 no X-PurpletGo-Signature header.
  • GET returns last_run (last delivery), last_attempt_at and last_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 (with X-Signing-Key-Id); scheduled-report emails attach it as <file>.pdf.sig. Verify it with the public key from GET /api/audit-log/signing-key, as described there.
  • Downloads also carry X-Document-Id and X-Content-SHA256.
  • A download whose audit entry can't be written is refused with 503 rather 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:

StatusWhen
400Invalid from/to (not a real date, only one given, from after to, too long a range)
403Missing analytics_view / analytics_export (the body has permission), or the plan doesn't include a gated feature (pdfExport, slaComplianceDashboard; the body has upgradeRequired)
404Offboarding not found or outside your scope (PDF export)
500Query or PDF generation failed
503The PDF could not be recorded in the audit log, so it was not sent

Related

  • Audit Log - the underlying event trail behind the compliance summary
  • Webhooks - a signed, event-driven alternative to polling these endpoints