Xero Reports — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Xero Reports (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Xero exposes its financial reports through the API as structured, programmatically parseable data. For MSPs, these reports drive profitability tracking by service line, client payment-behavior monitoring, cash flow management, and financial statements for stakeholders.
summarized rows with no drill-through; the underlying records are xero-invoices and xero-payments.
xero-accounts.qbo-reports.| Report | Endpoint | Description |
|---|---|---|
| Profit and Loss | /Reports/ProfitAndLoss | Revenue, expenses, and net profit |
| Balance Sheet | /Reports/BalanceSheet | Assets, liabilities, and equity |
| Aged Receivables | /Reports/AgedReceivablesByContact | Outstanding customer invoices by age |
| Aged Payables | /Reports/AgedPayablesByContact | Outstanding supplier bills by age |
| Trial Balance | /Reports/TrialBalance | All account balances at a point in time |
| Bank Summary | /Reports/BankSummary | Summary of bank account activity |
| Budget Summary | /Reports/BudgetSummary | Budget vs actual comparison |
| Executive Summary | /Reports/ExecutiveSummary | High-level financial overview |
All Xero reports share one nested Rows/Cells shape. Values arrive as strings and must be parsed to numbers:
{
"Reports": [
{
"ReportID": "ProfitAndLoss",
"ReportName": "Profit and Loss",
"ReportType": "ProfitAndLoss",
"ReportDate": "23 February 2026",
"UpdatedDateUTC": "/Date(1772006400000)/",
"Rows": [
{
"RowType": "Header",
"Cells": [
{ "Value": "" },
{ "Value": "1 Mar 2026 to 31 Mar 2026" }
]
},
{
"RowType": "Section",
"Title": "Revenue",
"Rows": [
{
"RowType": "Row",
"Cells": [
{ "Value": "Managed Services Revenue", "Attributes": [{ "Value": "acc-id-200" }] },
{ "Value": "45000.00", "Attributes": [{ "Value": "acc-id-200" }] }
]
}
]
},
{
"RowType": "SummaryRow",
"Cells": [
{ "Value": "Total Revenue" },
{ "Value": "67500.00" }
]
}
]
}
]
}| RowType | Description |
|---|---|
Header | Column headers |
Section | Group of related rows (e.g., Revenue, Expenses) |
Row | Individual data row (account or contact) |
SummaryRow | Total/subtotal row |
Reports are GET-only and take their parameters in the query string. Period reports (P&L, Bank Summary) use fromDate/toDate; point-in-time reports (Balance Sheet, Aged Receivables/Payables, Trial Balance) use a single date. All dates are YYYY-MM-DD.
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/ProfitAndLoss?fromDate=2026-03-01&toDate=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"Parameters worth knowing across reports:
| Parameter | Applies to | Description |
|---|---|---|
periods + timeframe | P&L, Balance Sheet | Adds comparison columns (MONTH, QUARTER, YEAR) |
trackingCategoryID / trackingOptionID | P&L, Balance Sheet | Filter to a service line or department |
paymentsOnly | P&L, Balance Sheet, Trial Balance | true = cash basis, false = accrual |
contactID | Aged Receivables/Payables | Restrict the report to one contact |
See references/api.md for the full request catalog, per-report parameter tables, and the complete endpoint reference.
Fetch P&L, Balance Sheet, Aged Receivables, and Aged Payables for the closing month in parallel, then parse each into a summary. Reports are independent requests, so parallelizing is safe and materially faster.
Walk the P&L Rows tree, matching Section titles (Revenue/Income, Less Cost of Sales/Direct Costs) and reading the SummaryRow totals. Section titles vary by chart-of-accounts layout — match on more than one candidate title.
Parse the Aged Receivables sections into per-contact aging buckets (current, 30, 60, 90, older) and sort by total overdue. Cells are positional, so index by column order rather than by name.
Request the same P&L period for the current and prior year, then compare parsed revenue totals.
See references/examples.md for working JavaScript implementations of each workflow.
| Code | Message | Resolution |
|---|---|---|
| 400 | Invalid date format | Use YYYY-MM-DD format |
| 400 | fromDate must be before toDate | Swap date parameters |
| 400 | Invalid tracking category | Verify trackingCategoryID exists |
| 401 | Unauthorized | Refresh access token |
| 403 | Insufficient scope | Ensure accounting.reports.read scope |
| 404 | Report not found | Check report endpoint name |
| Issue | Cause | Fix |
|---|---|---|
| Empty rows | No data for period | Verify date range has transactions |
| Missing sections | No revenue or expenses | Normal for new organizations |
| Null cell values | Account has no balance | Default to 0 when parsing |
fromDate/toDate or date yields Xero's default period, which is rarely the one you meant.paymentsOnly=true reports cash movements only; use it for cash flow analysis, not for revenue recognition.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.