Reports & Analytics
Breeze provides a full reporting and analytics subsystem for generating on-demand or scheduled fleet reports and querying real-time analytics data. Reports are scoped to organizations (the business reports can also run partner-wide) and enforce multi-tenant access at every layer – organization-scoped users see only their own data, partner-scoped users can access reports for any organization they manage, and system-scoped users have unrestricted access.
The system is split into two complementary route groups:
/reports– Saved report definitions, ad-hoc generation, run history, and raw data endpoints for device inventory, software, alerts, compliance, and performance metrics./analytics– Time-series queries, custom dashboards with widgets, capacity planning, SLA tracking, executive summaries, and OS distribution breakdowns.
Report Types
Section titled “Report Types”Every report has a type that determines which data is queried and how the output is structured.
| Type | Description | Key columns / metrics |
|---|---|---|
device_inventory |
Full hardware and software asset listing per device | hostname, OS, agent version, CPU model, RAM, disk, serial number |
software_inventory |
All installed software across targeted devices | software name, version, publisher, install date, device hostname |
alert_summary |
Alert history with severity breakdown | title, severity, status, triggered/acknowledged/resolved timestamps, rule name |
compliance |
Device health and compliance posture | hostname, OS, status, last seen, compliance flag, identified issues |
performance |
Aggregated CPU, RAM, and disk metrics per device | avg/max CPU %, avg/max RAM %, avg/max disk % |
executive_summary |
High-level fleet overview with device counts, alert stats, OS distribution, and site breakdown (see Executive Summary Report) | online/offline totals, health %, critical/high alert counts, resolution rate |
security_compliance_posture |
Per-control security and compliance evidence for one organization, built for cyber-insurance applications and vendor security questionnaires | EDR/AV coverage, disk encryption %, firewall %, patch currency %, password policy, local-admin exposure, MFA/PAM posture, backup, DNS filtering, optional CIS pass-rate (see Security & Compliance Posture Report) |
identity_access_review |
A month of Microsoft 365 sign-in and identity activity: identity inventory, conditional-access policy posture, dormant accounts, admin sign-ins, MFA registration status, and which remote-access client (if any) was running on each device | interactive sign-in counts, conditional-access coverage, dormant-account list, admin sign-in detail, MFA registration rate |
vulnerability_management |
Findings-level vulnerability detail: open vulnerabilities, accepted/mitigated exceptions, remediation ranking, and vulnerabilities that are actively exploited in the wild or have a high exploit probability | open/accepted/mitigated counts, remediation ranking, known-exploited and high-probability findings |
endpoint_management_review |
Device enrollment and compliance inventory pulled from Microsoft Intune, plus license-seat usage and a compliance-trend chart | enrollment/compliance counts, license seat usage, daily compliance trend |
threat_detection_review |
A record of threat detections for the period, with resolution status and the vendor’s recommendation text | detection count, resolution status, recommendation text |
ticket_sla_attainment |
Response and resolution SLA attainment for a period, recalculated from each ticket’s own timestamps rather than read off the stored breach flags, grouped by organization, priority, technician or category (see Business Reports) | response/resolution attainment %, missed targets, tickets with no SLA target |
technician_time_billability |
Logged time against an assumed weekly capacity, the billable / included / non-billable split, and how much billable time has been approved and billed — billable value reported per currency (see Business Reports) | utilization %, billable %, billing conversion %, logged/billable/non-billable minutes, billable value per currency |
ar_aging |
Unpaid invoice balances bucketed current / 1–30 / 31–60 / 61–90 / 90+ days past due, one set of totals per currency (see Business Reports) | outstanding balance per bucket, open invoice count, no-due-date balance, other open balance |
ai_usage_by_client |
AI requests, tokens, Breeze cost and the amount billable to each client for a period, split into billed, unbilled and unpriced (see AI usage chargeback) | requests, tokens, Breeze cost, billable amount, billed and unbilled amount, unpriced usage |
mrr_renewals |
Contract monthly recurring revenue as of the run, contracts up for renewal, and 12 months of billed recurring revenue, one set of totals per currency (see MRR & renewals) | MRR, ARR, base and overage, paused and pending activation, renewals due, billed recurring revenue per month |
quote_win_rate |
Share of sent quotes that were won, counted per opportunity (a quote and all its revisions), with the still-open count beside it, and quoted and won values per currency (see Quote win rate) | win rate, close rate, viewed rate, median days to win and to loss, quoted and won value per currency |
client_profitability |
Per-client billed revenue, cost from the invoice-line cost snapshot with its coverage, the revenue mix and technician hours for a period, with an optional estimated contribution after labor (see Client profitability) | revenue, cost, gross margin, margin %, cost coverage %, unknown-cost revenue, revenue mix, hours by coverage, effective service rate, estimated contribution after labor |
Report Configuration
Section titled “Report Configuration”When creating a saved report, you provide a config object that controls date ranges, filters, column selection, and sorting.
Date Range Presets
Section titled “Date Range Presets”The config.dateRange object supports preset shortcuts or custom boundaries:
| Preset | Meaning |
|---|---|
last_7_days |
Data from the past 7 days |
last_30_days |
Data from the past 30 days |
last_90_days |
Data from the past 90 days |
custom |
Use the explicit start and end ISO date strings |
A preset is a rolling window evaluated when each run generates, so a weekly report with last_7_days covers the seven days before that run. Explicit start/end values take precedence over a preset. In the Executive Summary the window bounds the alert figures (alerts in window, severity counts, resolution rate); the device figures are a snapshot of the fleet at generation time.
Filters
Section titled “Filters”The config.filters object narrows the data:
| Filter | Applies to | Accepts |
|---|---|---|
siteIds |
device_inventory, compliance | Array of site UUIDs |
deviceIds |
software_inventory | Array of device UUIDs |
osTypes |
device_inventory | windows, macos, linux |
status |
alert_summary | Array of status strings |
severity |
alert_summary | Array of severity strings |
Output Formats
Section titled “Output Formats”| Format | Description |
|---|---|
csv |
Comma-separated values (default) |
pdf |
Formatted PDF document |
excel |
Excel spreadsheet (.xlsx) |
Schedule Details and Email Recipients
Section titled “Schedule Details and Email Recipients”For recurring reports, the Schedule and delivery panel in the report builder (also available as config.schedule and config.emailRecipients via the API) controls exactly when the report runs and who gets emailed the result:
| Field | Builder UI label | Applies to | Format |
|---|---|---|---|
schedule.time |
Run time | All recurring cadences | 24-hour HH:MM, e.g. 09:00 |
schedule.day |
Day of week | weekly |
Weekday name, e.g. monday |
schedule.date |
Day of month | monthly |
Day of month, 1-31 |
emailRecipients |
Email distribution list | Any recurring cadence | Array of up to 50 email addresses |
See Scheduling below for how these are evaluated and what recipients receive.
Generating Reports
Section titled “Generating Reports”Breeze supports two generation flows: ad-hoc (stateless) and saved (tracked with run history).
Ad-hoc Generation
Section titled “Ad-hoc Generation”Use POST /reports/generate to produce report data immediately without creating a saved report definition. The response contains the generated data inline.
curl -X POST /api/v1/reports/generate \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "device_inventory", "format": "csv", "orgId": "ORG_UUID", "config": { "dateRange": { "preset": "last_30_days" }, "filters": { "osTypes": ["windows", "linux"] } } }'The response includes:
{ "type": "device_inventory", "format": "csv", "generatedAt": "2026-02-18T12:00:00.000Z", "data": { "rows": [...], "rowCount": 42 }}Saved Report Generation
Section titled “Saved Report Generation”For saved reports, trigger generation with POST /reports/:id/generate. This creates a report run, computes the data, and stores the result as a point-in-time snapshot – all in the same request, so the response comes back already completed (or failed). The snapshot is what you download later, so the file always reflects the data as of when the run executed rather than re-querying live.
- Create a saved report definition with
POST /reports(see the API reference below). - Trigger generation with
POST /reports/:id/generate. The response returns the newrunIdwithstatus: "completed"once the snapshot is stored. - Download the result with
GET /reports/runs/:runId/download?format=csv(orpdf/excel).
# Trigger generation for a saved reportcurl -X POST /api/v1/reports/REPORT_UUID/generate \ -H "Authorization: Bearer $TOKEN"Response:
{ "message": "Report generation started", "runId": "a1b2c3d4-...", "status": "pending"}Scheduling
Section titled “Scheduling”Saved reports support recurring schedules. When you set the schedule field, the report will be generated automatically on the configured cadence.
| Schedule | Behavior |
|---|---|
one_time |
No recurring generation (default). Must be triggered manually. |
daily |
Generated once per day |
weekly |
Generated once per week |
monthly |
Generated once per month |
Set the schedule when creating or updating a report:
curl -X POST /api/v1/reports \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Weekly Compliance Report", "type": "compliance", "orgId": "ORG_UUID", "schedule": "weekly", "format": "pdf", "config": { "filters": { "siteIds": ["SITE_UUID"] } } }'Scheduled runs fire in the organization’s local time zone (falling back to the partner’s time zone, then UTC if neither is configured), pinned to the schedule.time / schedule.day / schedule.date values from config.schedule – see Schedule Details and Email Recipients above.
A newly saved recurring report first runs at its first scheduled occurrence after it was saved – a monthly report for the 1st that is saved on the 21st first runs (and emails its recipients) on the next 1st. To get a copy straight away, use Generate now on the report (POST /reports/:id/generate).
Email Delivery
Section titled “Email Delivery”Add addresses to the Email distribution list (or config.emailRecipients) to have each scheduled run emailed automatically, in addition to appearing in the reports list:
- PDF-format reports attach the same branded PDF you’d download from the run. If it’s larger than 5 MB or fails to render, the email falls back to a link into Breeze instead of an attachment.
- CSV and Excel-format reports attach the data as a CSV file.
- The email leads with a one-line trend summary when a prior run exists to compare against, for example “Posture score 79 – up from 74 last run.”
The reports list also shows the next scheduled run time (e.g. “Next: Mon, Jul 6, 9:00 AM”) and a recipient count badge on recurring reports.
Scheduled Auto-Evidence
Section titled “Scheduled Auto-Evidence”You can also attach one of the report types above to a service deliverable as its evidence source. Set this up once, partner-wide, on a deliverable template item, and every organization the template applies to gets a period-correct evidence artifact generated automatically – no technician has to remember to run and attach a report each period. Each generated artifact is reviewed before it’s shown to the customer, and only becomes visible in the portal once its deliverable occurrence is marked Delivered.
Report Run Status Lifecycle
Section titled “Report Run Status Lifecycle”Each time a saved report is generated, a report run record tracks the process through a status lifecycle.
| Status | Meaning |
|---|---|
pending |
The run has been created and is queued for processing |
running |
The report is actively being generated |
completed |
Generation succeeded; outputUrl contains the download path |
failed |
Generation failed; errorMessage contains the reason |
The run record stores:
| Field | Description |
|---|---|
id |
Unique run identifier (UUID) |
reportId |
The parent saved report definition |
status |
Current lifecycle status |
startedAt |
Timestamp when processing began |
completedAt |
Timestamp when processing finished (success or failure) |
outputUrl |
Download path for the generated file (set on completed) |
errorMessage |
Error description (set on failed) |
rowCount |
Number of data rows in the output |
Security & Compliance Posture Report
Section titled “Security & Compliance Posture Report”The Security & Compliance Posture report (security_compliance_posture) packages your security evidence for one organization into a single document – purpose-built for filling out cyber-insurance applications and answering vendor security questionnaires. It joins the security data Breeze already collects into per-control answers with percent-implemented rollups: EDR/MDR and native antivirus coverage, the list of unprotected devices, disk-encryption and host-firewall coverage, patch currency, antivirus-definition freshness, password-complexity posture, local-admin exposure, MFA and privileged-access posture, backup coverage, DNS filtering, identity-provider connection, and an optional CIS hardening pass-rate.
It is scoped to a single organization (with an optional site filter) and is honest about gaps: a control that hasn’t been assessed is reported as N/A, never as a misleading 0% or a falsely favorable score. The same principle applies to stale readings: a device whose firewall/encryption status hasn’t been reported within the freshness window (default 30 days, configurable 1–365 via the report’s security-status age setting) is counted as unknown for those controls — its cells show “no data” instead of a months-old pass/fail being scored as current. The summary reports separate firewall-unknown and encryption-unknown device counts, mirroring how unmeasured patch data is handled. The PDF renders as a branded scorecard (overall score out of 100 with a STRONG / GOOD / FAIR / AT RISK band and a per-control grid) and carries point-in-time, not-a-certification disclaimers.
The Security products in use inventory follows the same principle. A product is listed with the number of devices it is installed on, and – where Breeze has per-device antivirus evidence – how many of those devices actually have real-time protection switched on. A product installed on 200 devices with real-time protection active on only one reads as 200 devices, 1 with real-time protection on, so the inventory can no longer imply full coverage that the report’s own metric grid contradicts. The extra count is shown only when the protected subset is smaller than the install count; products whose evidence comes from an integration rather than per-device status (DNS filtering, backup, Microsoft 365) show the install count alone.
The PDF also includes a Recommended actions section generated from whichever controls are currently failing, and a plain-language glossary so a non-technical reader – an insurance underwriter, a customer’s compliance contact – can follow it without help. When a prior run exists to compare against, the scorecard shows how the score has moved since then (for example “+5 since Jun 1”). The cover carries your logo and name if partner branding is configured; otherwise it uses the standard Breeze mark.
Executive Summary Report
Section titled “Executive Summary Report”The Executive Summary report (executive_summary) is built for handing to a customer at a quarterly business review. Its PDF leads with a designed cover: a fleet-health scorecard, device and alert metrics, OS and site composition, and a Recommended actions section – the same trend-aware, branded cover treatment as the Security & Compliance Posture report, including the partner logo when branding is configured and a score-change chip when a prior run exists to compare against.
Business Reports
Section titled “Business Reports”Several report types cover the PSA side of the business — service performance, technician time, money owed, contract revenue, and quote outcomes. Unlike the report types above, they are internal: they are for MSP staff only (organization-scope users, and API or MCP keys, never see them, even when they hold the underlying permission), they never appear in a customer’s portal, and generating one requires the type’s own permission on top of reports:read/reports:write – tickets:read for ticket SLA attainment, time_entries:read and tickets:read for technician time & billability, invoices:read for AR aging, contracts:read for MRR & renewals, quotes:read for quote win rate, and invoices:read and time_entries:read for client profitability.
Ownership: one organization, or all of them
Section titled “Ownership: one organization, or all of them”A business report can be owned by a single organization or by the partner. A partner-owned report covers every organization the MSP manages, resolved live at each run against the partner’s active and trial organizations – suspended, archived and deleted organizations (and the internal quick-support organization) are excluded, and are listed in the report’s own notes when they are. Ownership is chosen when the report is created and cannot be changed afterwards; create a second report if you need both views. In the reports list, a partner-owned report carries a Partner-wide badge and appears both in the All organizations view and when an organization is selected (for users with access to all organizations).
Creating or running a partner-owned report requires a partner-scope login with access to all organizations. A user restricted to selected organizations is refused: aggregating customers they cannot open individually would be an escalation, not a convenience. Business reports – org-owned or partner-owned – deliver to email addresses entered on the report itself; they cannot be delivered to a customer contact.
Ticket SLA attainment (ticket_sla_attainment)
Section titled “Ticket SLA attainment (ticket_sla_attainment)”Measures the share of support tickets created in the period that met their response and resolution targets. Attainment is recalculated from each ticket’s own timestamps and recorded paused minutes against its own targets, rather than read off the stored breach flags. Only support work is measured – planned work is excluded. Tickets with no target set are never counted as met or missed; they are reported as their own figure.
Grouping is by organization, priority, technician or category; left unset, it defaults to organization for a partner-owned report and priority for an org-owned one. Two limits are disclosed on the report itself:
- Paused time is a lifetime total – a ticket paused after its first reply has that pause counted against the response clock too, which slightly flatters response attainment.
- The technician axis is the ticket’s current assignee – reassignment history isn’t tracked.
Technician time & billability (technician_time_billability)
Section titled “Technician time & billability (technician_time_billability)”Measures logged time against capacity, and how much of it converts to revenue.
- Utilization is logged minutes ÷ capacity minutes. Capacity is a report setting – an assumed weekly-hours figure (1–80, default 40) – prorated over the working days in the period. This assumes uniform capacity: PTO and part-time schedules aren’t modeled, so an individual technician’s figure can mislead even when the team figure is sound. At organization scope the denominator is still each technician’s whole capacity, so the figure is a share of their capacity spent on that organization, not their overall utilization.
- Billable share is billable minutes ÷ logged minutes, split three ways – billable, included in plan, and non-billable.
- Billing conversion is approved-and-billed minutes ÷ billable minutes. It is deliberately not called “realization” – there is no fee-schedule baseline to compute financial realization against.
Technicians with no time in the period are listed at 0% rather than omitted – dropping them would inflate the team average. Billable value is reported once per currency, never summed across currencies; billable minutes with no hourly rate or currency are called out and excluded from that total. An organization-scoped report covers ticket-linked time only – time logged with no organization has no organization to be scoped to.
AR aging (ar_aging)
Section titled “AR aging (ar_aging)”Buckets unpaid invoice balances by how far past due they are: current, 1–30, 31–60, 61–90 and 90+ days, plus two reconciliation buckets:
- No due date – an open balance on an invoice with no due date is aged into its own bucket, never silently into “current”.
- Other open balance – an invoice carrying a balance in an unexpected status appears here rather than vanishing, so buckets plus this figure always reconcile to total AR.
Aging is measured as of the moment the report runs, in the report owner’s resolved timezone, unless an explicit as-of date is set. The as-of date moves only the aging reference date – it does not reconstruct historical balances or undo payments recorded after it. An optional note can also list invoices that were fully paid month-to-date as of that date; it’s informational and never moves a bucket. Totals are reported once per currency, with no conversion – there is deliberately no combined figure, because an FX-converted headline would be wrong the day after it was produced.
MRR & renewals (mrr_renewals)
Section titled “MRR & renewals (mrr_renewals)”Reports contract revenue as of the moment the report runs. Contracts keep no status history, so this report has no period and no as-of date – it cannot be backdated, and sending period or asOf returns 400. Dates are compared as UTC calendar dates, the same “today” contract billing uses.
What MRR counts. A contract counts while it is in term: started, not yet past its end date, and either still billing or in a final term that is already billed and still being served. Each contract’s monthly figure is its billing-period value divided by the billing interval, rounded in the contract’s own currency, and is split into:
- Base – the recurring lines at their billing prices.
- Overage – billed allowance overage, counted from today’s device and seat quantities.
- Final term billed – a contract whose last period is already invoiced (for example, a one-year contract billed annually in advance) counts at what that invoice billed. That figure is not split into base and overage.
Per-device and per-seat quantities are counted live when the report runs, so MRR is an estimate of the next invoices, not a billed amount. ARR is MRR × 12. Totals are reported once per currency, with no conversion.
What sits beside MRR, never in it. Paused contracts and draft contracts (pending activation) are valued the same way and shown next to MRR. Open-ended contracts (no end date) are part of MRR and are also shown as their own figure. Two reconciliation lists make sure nothing disappears silently:
- Active, not in term – contracts marked active that are not counted, with the reason: they start later, they ended and are waiting to be marked expired, or their final period has not been billed.
- Could not be valued – contracts left out of MRR because a line bills a device group that could not be evaluated, a deleted site, or a final-term invoice that is missing or voided. The report says how many were excluded.
Renewals. In-term contracts whose end date falls inside the renewal window (30, 60, 90 or 180 days from the run date, default 90) are listed by end date, with auto-renewing and manually renewed MRR totalled separately, and the date the advance renewal notice was sent for that end date.
Billed recurring revenue. The 12 calendar months ending with the current month (in the report owner’s time zone) show what issued invoices billed for contracts. A billing period’s line is spread evenly over the months it covers; a contract line added to an invoice by hand lands in the invoice’s month. Hour-block overage is usage, not recurring revenue, and is left out. The current month is marked partial. This series is billed revenue, not MRR.
MRR has no stored history before the report first runs. Schedule it monthly, and each run’s PDF compares MRR with the previous run (“vs previous observation”).
Quote win rate (quote_win_rate)
Section titled “Quote win rate (quote_win_rate)”Reports how many of the quotes you sent were won. The unit is an opportunity, not a quote row: a quote and every revision of it (decline, revise, send again) is one opportunity, and its outcome is the outcome of the latest version that was actually sent. A draft revision that has not been sent leaves the sent version’s outcome standing.
Which opportunities are in the period. An opportunity belongs to the period in which its first version was sent. A quote that was never sent is not counted. The period is resolved in the report owner’s time zone.
Outcomes. Won is accepted or converted, lost is declined, expired is a quote past its expiry date, and open is still waiting. A quote that is still marked sent or viewed after its expiry date counts as expired when the report runs (UTC calendar day), even if the nightly expiry job has not touched it yet.
The headline. Win rate is won ÷ (won + lost + expired) – decided opportunities only – and the number of opportunities still open is printed beside it, so a recent period that has not played out yet does not look worse than it will end up. With nothing decided, the win rate prints N/A, never 0%. Close rate (won ÷ all opportunities), viewed rate and the median days from first send to a win and to a loss sit underneath.
Values. Quoted and won values are pre-tax, taken from the latest sent version, and reported once per currency, with no conversion. One-time and monthly recurring values are separate figures; an annual recurring amount is converted to a monthly one. The value win rate (won value ÷ decided value) is per currency too.
Grouping. Group by organization, author (the person who created the original quote) or currency. The default is organization for a partner-wide report and author for a single organization. Detail rows list each opportunity with its version count, outcome and, for declines, the reason given.
What it does not do. Accepted quotes create draft contracts, and this report does not follow them any further – see MRR & renewals for contracts pending activation. A broken or circular revision chain is reported as a note and left out of every rate.
Client profitability (client_profitability)
Section titled “Client profitability (client_profitability)”For each client in the period: what you billed, what the billed goods and services cost you, and how much technician time the client used. Generating it needs invoices:read and time_entries:read.
Billed basis. Revenue is what issued invoices billed, counted on the invoice’s issue date (in the report owner’s time zone). Draft and void invoices are left out; a voided invoice’s replacement counts once. This is billed revenue, not accrual-recognized revenue: an annual contract invoiced in January counts entirely in January. Hours are counted when they were worked, and the invoice for that work can fall in a later period.
Cost and margin. Cost comes from the cost recorded on each invoice line when it was created. Only top-level, customer-visible lines are counted, so a bundle’s cost is counted once (on the bundle line, not again on its components). Each line’s cost is quantity × recorded cost, rounded to the currency’s smallest unit.
- Unknown cost is not zero cost. Time, hour-block overage, AI usage and most contract lines carry no recorded cost. Their revenue is reported as unknown-cost revenue and is left out of the margin. A line with an explicit cost of
0.00is a known zero. - Gross margin and margin % are measured only over lines with a recorded cost. Cost coverage % – the share of revenue that has a recorded cost – is printed beside every margin, so a client billed mostly for labor never shows a misleading margin taken from a sliver of its revenue.
- A bundle with a component that has no recorded cost counts as unknown cost here, and the report says how many bundles that affected. (The invoice margin panel counts the missing component as zero; this is the one place the two differ.)
- Products sold through a quote carry their cost only if the quote was accepted after quote costs began to be copied onto invoices; earlier ones count as unknown cost.
Revenue mix. Each line is filed by its source: recurring (contract lines), labor (time entries, and hour-block overage, which is usage), product (parts, catalog and bundle lines, and catalog items sold through a quote), AI (AI usage) and other (the remaining manual lines). AI lines carry no cost here; the AI usage by client report shows what the AI cost beside the charges.
Hours and effective service rate. Hours are completed time entries (running timers excluded) whose start falls in the period, split into billable, included and non-billable. The effective service rate is (recurring + labor revenue) ÷ hours worked; product resale and AI are left out so a hardware-heavy month does not flatter a client. Revenue per hour is printed beside it. Both are blank when no hours were worked. Partner-wide, time entries with no organization are shown once as unattributed time, never spread across clients.
Hours and currencies. A client’s hours are attributed to a currency only when all of its revenue in the period is in one currency and every one of its time entries is in that same currency. Otherwise the hours are shown once on the client and its rate and labor-estimate figures are blank. Hours are never split or repeated across currencies.
Estimated contribution after labor (optional). Set a uniform internal labor cost per hour on the report (an amount and a currency) to add an estimate:
estimated contribution after labor = gross margin + labor and recurring revenue with no recorded cost − all completed hours × the rate
- It counts all completed hours – billable, included and non-billable – because all of them cost you.
- Recurring revenue with no recorded cost is treated as service revenue (the included time is the cost of serving a managed contract). That share is printed on its own line (“of which recurring revenue with no recorded cost”), so a resold licence billed through a contract is visible as an assumption.
- Product, AI and other revenue with no recorded cost is left out of the estimate and printed beside it.
- It is one blended rate for every technician, and it assumes no labor is already inside a line’s recorded cost. Every figure that uses it is labelled Estimated contribution after labor (uniform rate).
- The rate applies only to clients whose hours are attributed to the rate’s currency; no FX conversion is applied. A client with revenue but no completed time logged in the period gets no estimate – unlogged time is not a zero labor cost. With no rate set, the report shows hours only and no estimate.
- Partner-wide, a currency’s rate and estimate figures are left blank when any client billed in that currency cannot have its hours attributed to it, so they always cover the same clients as the margin beside them.
The labor rate is an option of this report, not a partner setting: it is stored on the report definition, edited in the report’s options, and never appears on a customer document.
Partner-wide, one detail row is stored per client and currency (plus one hours-only row for a client whose hours cannot sit on a single currency row, and one for unattributed time); one organization’s report stores one row per invoice. Totals are reported once per currency, with no conversion.
What gets stored, and what gets capped
Section titled “What gets stored, and what gets capped”Every total is computed in SQL over all matching rows. The stored detail rows – the per-ticket, per-technician or per-invoice lines behind the totals – are capped at 5,000 per run; a truncated detail list never implies a truncated total. A downloaded PDF shows up to 500 detail rows; CSV and Excel downloads of a stored run carry the full (capped) set as raw numeric values – balances and rates as plain decimals, durations as minutes, percentages as fractions – each money row tagged with a currency-code column, so a spreadsheet can total them directly. A ratio with nothing to measure (for example, no tickets with a response target) prints N/A, never 0%. A user whose access is limited to specific sites gets an explanatory note instead of figures, because tickets, time entries, and invoices are not tied to sites.
API options
Section titled “API options”The web options forms set the same keys. Business reports select their data by period and owner (one organization, or partner-wide) – never by the date range, site, device, or organization filters the other report types use. Sending any of those filters returns 400.
| Option | Applies to | Values | Default |
|---|---|---|---|
period.kind |
SLA attainment, time & billability | last_full_month, last_30_days, last_quarter, custom (with start and end as YYYY-MM-DD; end is inclusive) |
last_full_month |
groupBy |
SLA attainment | organization, priority, technician, category |
organization partner-wide, priority for one organization |
includeNoSla |
SLA attainment | Include tickets with no SLA target in the detail rows | true |
groupBy |
Time & billability | technician, organization, work_type |
technician |
weeklyCapacityHours |
Time & billability | 1-80 | 40 |
asOf |
AR aging | YYYY-MM-DD |
Today |
groupBy |
AR aging | organization, currency |
organization |
includePaidInPeriod |
AR aging | Add an informational note on invoices fully paid month-to-date (never moves a bucket) | false |
groupBy |
MRR & renewals | organization, contract |
organization |
renewalHorizonDays |
MRR & renewals | 30, 60, 90, 180 |
90 |
period.kind |
Quote win rate | last_full_month, last_30_days, last_quarter, custom (applies to each opportunity’s first send date) |
last_full_month |
groupBy |
Quote win rate | organization, author, currency |
organization partner-wide, author for one organization |
period.kind |
Client profitability | Same values as above | last_full_month |
groupBy |
Client profitability | organization, source (revenue category) |
organization partner-wide, source for one organization |
laborCostPerHour |
Client profitability | { "amount": "45.00", "currencyCode": "USD" } – a positive amount with at most 2 decimals (whole units for zero-decimal currencies) in a supported currency |
Not set (hours only, no estimate) |
Periods and “today” are resolved in the report owner’s time zone – the organization’s, then the partner’s, then UTC. If the configured time zone isn’t recognized, the report runs in UTC and says so.
Creating a partner-wide report through the API
Section titled “Creating a partner-wide report through the API”Pass "ownerScope": "partner" and omit orgId. The report belongs to your partner, not to any one organization; ownerScope is set at creation and can’t be changed later.
curl -X POST /api/v1/reports \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Monthly SLA attainment (all customers)", "type": "ticket_sla_attainment", "ownerScope": "partner", "schedule": "monthly", "format": "pdf", "config": { "period": { "kind": "last_full_month" }, "groupBy": "organization", "schedule": { "time": "08:00", "date": 1 }, "emailRecipients": ["[email protected]"] } }'For a one-off, POST /reports/generate accepts the same ownerScope, type, and config and returns the data inline. To run a report for a single customer instead, leave ownerScope out and pass orgId as you would for any other report type.
Scheduled business reports are delivered only to the email distribution list (config.emailRecipients); they can’t be sent to organization contacts.
Multi-org reports (one per organization)
Section titled “Multi-org reports (one per organization)”A multi-org report is one definition that produces a separate copy for every organization it covers. Each copy is sent to that organization’s own contacts. It is not a combined report: combined, cross-organization reports are the partner-owned reports described under Business Reports.
Only partner users with access to every organization can create or change one.
Creating one
Section titled “Creating one”On New Report (or a Hardware Lifecycle / Security & Compliance Posture template), set Covers to One report per organization, then choose which organizations it covers:
- All organizations (default): every current and future organization is included automatically. Untick any organization to exclude it. A new organization starts receiving the report within about five minutes of being created.
- Chosen organizations: exactly the organizations you tick.
Only active and trial organizations receive a copy. A suspended or offboarding organization’s copy is archived, and it comes back automatically if the organization is reactivated.
Site, device and group filters are unavailable here, because each organization’s copy covers the whole organization.
A multi-org report always runs on a schedule (daily, weekly or monthly); it can’t be one-time. When you start from a one-time template, choose how often it is sent before creating it.
Who receives each copy
Section titled “Who receives each copy”- The organization’s primary contact and/or contacts with these roles. This rule is worked out separately in every organization.
- Internal copies: fixed addresses on your team, sent every organization’s copy.
- The form previews the result, for example “Resolves to 23 contacts across 17 organizations · 1 organization has no customer recipient: Acme Dental”. An organization with no customer recipient is always flagged, never silently skipped.
To change who receives one organization’s copy, open that copy (Edit recipients). For each contact, choose Follow rule, Always send or Never send.
The Saved Reports list
Section titled “The Saved Reports list”On the All organizations view, each multi-org report is one row. Its last-run column reads, for example, Oct 1 · 17/18 delivered · 1 no recipient. Expand the row to see one line per organization, with these actions:
- Run now generates that organization’s copy immediately.
- Edit recipients opens that organization’s copy.
- Exclude / Include stops or restarts that organization’s copy. An excluded copy is archived with its run history, and including it again restores the same copy.
- Detach turns that organization’s copy into a standalone report you can edit freely. It keeps its run history and stops following the multi-org report. While that standalone copy exists, the organization stays out of the multi-org report, even if you include it again, so it never receives two copies.
Pause (on the row, or at the top of the expanded list) stops every organization’s copy from running until you Resume it. A paused report is marked Paused in the list.
The All · Multi-org · Single-org · Combined chips filter the list. With one organization selected in the switcher, its copy appears as an ordinary row with a Multi-org badge that links to the multi-org report.
An organization’s copy is locked: only its recipients can be changed there. Organization users see it marked Managed by your MSP.
Scheduled copies run with the owner’s access (the creator, by default). If the owner loses access to an organization, that organization’s copy shows Blocked: owner has no access and does not run. It is never run with anyone else’s access. Use Transfer owner on the row to hand the report to another partner user with access to every organization.
Downloading and Exporting
Section titled “Downloading and Exporting”When a report run reaches the completed status, download its stored snapshot from GET /reports/runs/:id/download. Pass the desired format as a query parameter – ?format=csv, ?format=pdf, or ?format=excel. Because the run captured the data at generation time, the download is stable: re-downloading the same run always yields the same data. Downloading a run that hasn’t completed returns 409 Conflict.
# Download a completed run as CSVcurl "/api/v1/reports/runs/RUN_UUID/download?format=csv" \ -H "Authorization: Bearer $TOKEN" -OJCSV and Excel files are produced on the server; PDF downloads are rendered in the browser from the same snapshot (scheduled-email PDF attachments are rendered server-side instead, using the same branded layout, so the two always match). When retrieving a single report via GET /reports/:id, the response includes the five most recent runs, making it easy to find the latest completed download.
Report History for Inactive Organizations
Section titled “Report History for Inactive Organizations”When a customer organization is suspended, churned, offboarding or archived, its report history stays readable to the MSP: a partner user can still open the organization’s saved report definitions and its run history (status, dates, row counts, errors) – on the organization’s record page under Report history, or through the API with an explicit organization:
GET /reports?orgId=ORG_UUIDandGET /reports/templates?orgId=ORG_UUIDGET /reports/:idGET /reports/runs?orgId=ORG_UUIDandGET /reports/runs/:id
It is read-only. New runs, schedules, edits, recipient lists, downloads (/reports/runs/:id/download) and the raw data endpoints (/reports/data/*) stay refused for an inactive organization, and its devices, scripts and other records stay unavailable. The unfiltered lists (GET /reports, GET /reports/runs without orgId) keep showing active and trial organizations only.
Who can read it: an active user of an active partner whose role grants reports:read. A user limited to selected organizations sees only the inactive organizations on their list; a user with no organization access sees none. A deleted organization’s history is never shown.
Analytics Endpoints
Section titled “Analytics Endpoints”The /analytics route group provides real-time operational intelligence beyond static reports.
Time-Series Queries
Section titled “Time-Series Queries”POST /analytics/query executes a flexible time-series query across device metrics.
curl -X POST /api/v1/analytics/query \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "deviceIds": ["DEVICE_UUID_1", "DEVICE_UUID_2"], "metricTypes": ["cpu", "ram"], "startTime": "2026-02-01T00:00:00Z", "endTime": "2026-02-18T00:00:00Z", "aggregation": "avg", "interval": "hour" }'Supported aggregations: avg, min, max, sum, count, p95, p99.
Supported intervals: minute, hour, day, week, month.
An optional groupBy array lets you split the series by additional dimensions.
Custom Dashboards and Widgets
Section titled “Custom Dashboards and Widgets”Dashboards are per-organization containers for widgets. Each widget has a type and a config object that determines what data it renders.
curl -X POST /api/v1/analytics/dashboards \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "orgId": "ORG_UUID", "name": "NOC Overview", "description": "Primary network operations dashboard", "layout": {} }'curl -X POST /api/v1/analytics/dashboards/DASHBOARD_UUID/widgets \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "CPU Heatmap", "type": "heatmap", "config": { "metricType": "cpu", "interval": "hour" }, "layout": { "x": 0, "y": 0, "w": 6, "h": 4 } }'Executive Summary
Section titled “Executive Summary”GET /analytics/executive-summary returns a high-level organizational overview including device totals (online/offline), a 12-week enrollment trend, and configurable period types.
| Query parameter | Values | Default |
|---|---|---|
periodType |
daily, weekly, monthly |
monthly |
range |
Freeform string | – |
startDate |
ISO date string | – |
endDate |
ISO date string | – |
OS Distribution
Section titled “OS Distribution”GET /analytics/os-distribution returns the count of active devices grouped by osType and osVersion, excluding decommissioned devices.
Capacity Planning
Section titled “Capacity Planning”GET /analytics/capacity returns capacity predictions based on current metric trends. Accepts optional deviceId and metricType query parameters to narrow scope.
SLA Tracking
Section titled “SLA Tracking”SLA definitions set uptime or performance targets that are evaluated over a window.
| Field | Description |
|---|---|
name |
Human-readable SLA name |
targetPercentage |
Target compliance percentage (0–100) |
evaluationWindow |
daily, weekly, or monthly |
scope |
device, site, or organization |
filters |
Additional scoping filters (JSON) |
Compliance history for an SLA is retrieved via GET /analytics/sla/:id/compliance, which returns period-by-period entries with a status of met, breached, or warning.
Report Data Endpoints
Section titled “Report Data Endpoints”The /reports/data/* endpoints provide raw, paginated data used to power report views and dashboards. These are read-only query endpoints that accept common filters.
| Endpoint | Returns |
|---|---|
GET /reports/data/device-inventory |
Device list with hardware details (CPU, RAM, disk, serial number, manufacturer, model) |
GET /reports/data/software-inventory |
Installed software list with per-title device counts in a summary array |
GET /reports/data/alerts-summary |
Alert statistics: counts by severity, by status, daily trend (last 30 days), and top 10 alerting rules |
GET /reports/data/compliance |
Compliance overview with device status breakdown, OS distribution, agent version spread, and identified issues (stale devices, outdated agents) |
GET /reports/data/metrics |
Performance metrics: fleet-wide averages for CPU/RAM/disk plus the top 10 consumers for each metric |
All data endpoints accept these common query parameters:
| Parameter | Description |
|---|---|
orgId |
Filter to a specific organization (required for partner scope) |
siteId |
Filter to a specific site |
startDate |
Start of date range (ISO string) |
endDate |
End of date range (ISO string) |
limit |
Page size (default 100, max 1000) |
offset |
Pagination offset |
API Reference
Section titled “API Reference”Reports CRUD
Section titled “Reports CRUD”| Method | Endpoint | Description |
|---|---|---|
GET |
/reports |
List saved reports. Filter by type, schedule, orgId, and ownerScope (partner = only reports owned partner-wide across all organizations, organization = only single-organization reports). ownerScope only narrows the list: a caller who cannot manage partner-wide reports (organization logins, partner users with selected-organization access) gets an empty list for ownerScope=partner. Paginated. |
POST |
/reports |
Create a saved report definition with name, type, config, schedule, and format. Pass ownerScope: "partner" (and no orgId) for a partner-wide business report. |
GET |
/reports/:id |
Get a single report with its 5 most recent runs. |
PUT |
/reports/:id |
Update report name, config, schedule, or format. |
DELETE |
/reports/:id |
Delete a report and all associated runs. |
Report Generation
Section titled “Report Generation”| Method | Endpoint | Description |
|---|---|---|
POST |
/reports/generate |
Ad-hoc generation. Returns data inline. Accepts ownerScope: "partner" for business report types. |
POST |
/reports/:id/generate |
Trigger async generation for a saved report. Returns a runId. |
Report Runs
Section titled “Report Runs”| Method | Endpoint | Description |
|---|---|---|
GET |
/reports/runs |
List runs. Filter by orgId (one organization), reportId, status. Paginated. |
GET |
/reports/runs/:id |
Get a single run with parent report metadata and download URL. |
GET |
/reports/runs/:id/download |
Download the run’s stored snapshot. Requires ?format=csv|pdf|excel. Returns 409 if the run is not completed. |
Report Data
Section titled “Report Data”| Method | Endpoint | Description |
|---|---|---|
GET |
/reports/data/device-inventory |
Raw device inventory with hardware join. |
GET |
/reports/data/software-inventory |
Raw software inventory with per-title summary. |
GET |
/reports/data/alerts-summary |
Alert breakdown by severity, status, daily trend, top rules. |
GET |
/reports/data/compliance |
Compliance overview, stale device detection, agent version audit. |
GET |
/reports/data/metrics |
Fleet performance averages and top-10 resource consumers. |
Analytics
Section titled “Analytics”| Method | Endpoint | Description |
|---|---|---|
POST |
/analytics/query |
Execute a time-series metrics query with aggregation and interval. |
GET |
/analytics/dashboards |
List dashboards. Filter by orgId. Paginated. |
POST |
/analytics/dashboards |
Create a dashboard. |
GET |
/analytics/dashboards/:id |
Get dashboard with all widgets. |
PATCH |
/analytics/dashboards/:id |
Update dashboard name, description, or layout. |
DELETE |
/analytics/dashboards/:id |
Delete dashboard and all its widgets. |
POST |
/analytics/dashboards/:id/widgets |
Add a widget to a dashboard. |
PATCH |
/analytics/widgets/:id |
Update widget name, type, config, or layout. |
DELETE |
/analytics/widgets/:id |
Remove a widget from its dashboard. |
GET |
/analytics/capacity |
Capacity planning predictions. |
GET |
/analytics/sla |
List SLA definitions. Filter by orgId. Paginated. |
POST |
/analytics/sla |
Create an SLA definition. |
GET |
/analytics/sla/:id/compliance |
Get compliance history for an SLA. |
GET |
/analytics/executive-summary |
Executive fleet summary with enrollment trends. |
GET |
/analytics/os-distribution |
OS type/version distribution for active devices. |
GET |
/analytics/anomalies/evaluation |
Anomaly detection quality counts and rates for a range of 7d, 30d (default), or 90d. Filter by orgId or deviceId. See AI Insights. |
Troubleshooting
Section titled “Troubleshooting”Report run returns failed
Section titled “Report run returns failed”POST /reports/:id/generate computes the report and stores its snapshot in the same request, so a run normally comes back completed immediately. If it comes back failed, inspect the run’s errorMessage field (via GET /reports/runs/:id) for the cause – most often a configuration problem such as an invalid date range or a scope the caller can’t access.
“Organization context required” (403) on report endpoints
Section titled ““Organization context required” (403) on report endpoints”This error occurs when an organization-scoped token is used but the token lacks an orgId claim. Verify that the JWT includes the orgId field. Partner-scoped users must pass orgId explicitly as a query parameter or in the request body.
Data endpoint returns empty results despite existing devices
Section titled “Data endpoint returns empty results despite existing devices”The /reports/data/* endpoints join against related tables (deviceHardware, deviceSoftware, deviceMetrics). If devices exist but related data has not been collected yet (e.g., hardware inventory has not been reported by the agent), joins may exclude those devices or return null fields. Check that agents are running and have submitted at least one heartbeat with hardware and software data.
Compliance score unexpectedly low
Section titled “Compliance score unexpectedly low”The compliance report flags a device as non-compliant if its status is decommissioned or if it has not been seen in the last 7 days. Devices that are powered off or in maintenance for extended periods will lower the compliance score. The compliance data endpoint also identifies stale_devices (not seen in 7+ days) and outdated_agents (running a version other than the most common version) as separate issues. Review the issues array in the response to understand which factors are contributing to the score.