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 provides a comprehensive set of financial reports through the API. For MSPs, these reports are essential for tracking profitability by service line, monitoring client payment behavior, managing cash flow, and producing financial statements for stakeholders. The Reports API returns structured data that can be programmatically analyzed.
| 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 follow a consistent structure:
{
"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 |
# Current month P&L
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"
# Year-to-date P&L
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/ProfitAndLoss?fromDate=2026-01-01&toDate=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# P&L with tracking category filter
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/ProfitAndLoss?fromDate=2026-03-01&toDate=2026-03-31&trackingCategoryID=${TRACKING_ID}&trackingOptionID=${OPTION_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# P&L with monthly periods
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/ProfitAndLoss?fromDate=2026-01-01&toDate=2026-03-31&periods=3&timeframe=MONTH" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"P&L Parameters:
| Parameter | Type | Description |
|---|---|---|
fromDate | string | Start date (YYYY-MM-DD) |
toDate | string | End date (YYYY-MM-DD) |
periods | integer | Number of comparison periods |
timeframe | string | MONTH, QUARTER, or YEAR |
trackingCategoryID | string | Filter by tracking category |
trackingOptionID | string | Filter by tracking option |
standardLayout | boolean | Use standard layout (true/false) |
paymentsOnly | boolean | Cash basis (true) or accrual (false) |
# Balance Sheet as of today
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/BalanceSheet?date=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# Balance Sheet with comparison periods
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/BalanceSheet?date=2026-03-31&periods=3&timeframe=MONTH" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"Balance Sheet Parameters:
| Parameter | Type | Description |
|---|---|---|
date | string | Report date (YYYY-MM-DD) |
periods | integer | Number of comparison periods |
timeframe | string | MONTH, QUARTER, or YEAR |
trackingCategoryID | string | Filter by tracking category |
standardLayout | boolean | Use standard layout |
paymentsOnly | boolean | Cash basis reporting |
# Aged Receivables as of today
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/AgedReceivablesByContact?date=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# Aged Receivables for a specific contact
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/AgedReceivablesByContact?date=2026-03-31&contactID=${CONTACT_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# Aged Receivables with custom aging periods
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/AgedReceivablesByContact?date=2026-03-31&fromDate=2025-01-01&toDate=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"Aged Receivables Parameters:
| Parameter | Type | Description |
|---|---|---|
date | string | Report date (YYYY-MM-DD) |
contactID | string | Filter to specific contact |
fromDate | string | Start of aging period |
toDate | string | End of aging period |
# Aged Payables as of today
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/AgedPayablesByContact?date=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"# Trial Balance as of end of quarter
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/TrialBalance?date=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# Trial Balance with payments only (cash basis)
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/TrialBalance?date=2026-03-31&paymentsOnly=true" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"Trial Balance Parameters:
| Parameter | Type | Description |
|---|---|---|
date | string | Report date (YYYY-MM-DD) |
paymentsOnly | boolean | Cash basis (true) or accrual (false) |
curl -s -X GET "https://api.xero.com/api.xro/2.0/Reports/BankSummary?fromDate=2026-03-01&toDate=2026-03-31" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"async function monthlyFinancialReview(month) {
const fromDate = `${month}-01`;
const lastDay = new Date(parseInt(month.split('-')[0]), parseInt(month.split('-')[1]), 0).getDate();
const toDate = `${month}-${lastDay}`;
const token = await auth.getToken();
const headers = {
'Authorization': `Bearer ${token}`,
'xero-tenant-id': process.env.XERO_TENANT_ID,
'Accept': 'application/json'
};
// Fetch all reports in parallel
const [pnl, balanceSheet, agedReceivables, agedPayables] = await Promise.all([
fetch(`https://api.xero.com/api.xro/2.0/Reports/ProfitAndLoss?fromDate=${fromDate}&toDate=${toDate}`, { headers }),
fetch(`https://api.xero.com/api.xro/2.0/Reports/BalanceSheet?date=${toDate}`, { headers }),
fetch(`https://api.xero.com/api.xro/2.0/Reports/AgedReceivablesByContact?date=${toDate}`, { headers }),
fetch(`https://api.xero.com/api.xro/2.0/Reports/AgedPayablesByContact?date=${toDate}`, { headers })
]);
return {
profitAndLoss: await pnl.json(),
balanceSheet: await balanceSheet.json(),
agedReceivables: await agedReceivables.json(),
agedPayables: await agedPayables.json()
};
}function parseRevenueFromPnL(reportData) {
const report = reportData.Reports[0];
const revenue = { total: 0, accounts: [] };
for (const row of report.Rows) {
if (row.RowType === 'Section' && row.Title === 'Revenue') {
for (const subRow of row.Rows || []) {
if (subRow.RowType === 'Row') {
const name = subRow.Cells[0]?.Value;
const amount = parseFloat(subRow.Cells[1]?.Value) || 0;
revenue.accounts.push({ name, amount });
revenue.total += amount;
}
if (subRow.RowType === 'SummaryRow') {
revenue.total = parseFloat(subRow.Cells[1]?.Value) || revenue.total;
}
}
}
}
return revenue;
}function parseAgedReceivables(reportData) {
const report = reportData.Reports[0];
const clients = [];
for (const row of report.Rows) {
if (row.RowType === 'Section') {
for (const subRow of row.Rows || []) {
if (subRow.RowType === 'Row') {
const cells = subRow.Cells;
const client = {
name: cells[0]?.Value,
current: parseFloat(cells[1]?.Value) || 0,
thirtyDays: parseFloat(cells[2]?.Value) || 0,
sixtyDays: parseFloat(cells[3]?.Value) || 0,
ninetyDays: parseFloat(cells[4]?.Value) || 0,
older: parseFloat(cells[5]?.Value) || 0,
total: parseFloat(cells[6]?.Value) || 0
};
client.totalOverdue = client.thirtyDays + client.sixtyDays +
client.ninetyDays + client.older;
if (client.total > 0) {
clients.push(client);
}
}
}
}
}
return clients.sort((a, b) => b.totalOverdue - a.totalOverdue);
}function calculateGrossMargin(reportData) {
const report = reportData.Reports[0];
let totalRevenue = 0;
let totalCOGS = 0;
for (const row of report.Rows) {
if (row.RowType === 'Section') {
if (row.Title === 'Revenue' || row.Title === 'Income') {
for (const subRow of row.Rows || []) {
if (subRow.RowType === 'SummaryRow') {
totalRevenue = parseFloat(subRow.Cells[1]?.Value) || 0;
}
}
}
if (row.Title === 'Less Cost of Sales' || row.Title === 'Direct Costs') {
for (const subRow of row.Rows || []) {
if (subRow.RowType === 'SummaryRow') {
totalCOGS = parseFloat(subRow.Cells[1]?.Value) || 0;
}
}
}
}
}
const grossProfit = totalRevenue - totalCOGS;
const grossMargin = totalRevenue > 0 ? (grossProfit / totalRevenue * 100) : 0;
return {
revenue: totalRevenue,
costOfSales: totalCOGS,
grossProfit,
grossMarginPercent: grossMargin.toFixed(1)
};
}async function yearOverYearComparison(month) {
const year = parseInt(month.split('-')[0]);
const mon = month.split('-')[1];
const currentFrom = `${year}-${mon}-01`;
const currentTo = `${year}-${mon}-28`;
const priorFrom = `${year - 1}-${mon}-01`;
const priorTo = `${year - 1}-${mon}-28`;
const [current, prior] = await Promise.all([
fetchReport('ProfitAndLoss', { fromDate: currentFrom, toDate: currentTo }),
fetchReport('ProfitAndLoss', { fromDate: priorFrom, toDate: priorTo })
]);
const currentRevenue = parseRevenueFromPnL(current);
const priorRevenue = parseRevenueFromPnL(prior);
const growth = priorRevenue.total > 0
? ((currentRevenue.total - priorRevenue.total) / priorRevenue.total * 100)
: 0;
return {
currentPeriod: `${year}-${mon}`,
priorPeriod: `${year - 1}-${mon}`,
currentRevenue: currentRevenue.total,
priorRevenue: priorRevenue.total,
growthPercent: growth.toFixed(1)
};
}| 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 |
| Endpoint | Method | Description |
|---|---|---|
/Reports/ProfitAndLoss | GET | Profit and Loss statement |
/Reports/BalanceSheet | GET | Balance Sheet |
/Reports/AgedReceivablesByContact | GET | Aged Receivables by contact |
/Reports/AgedPayablesByContact | GET | Aged Payables by contact |
/Reports/TrialBalance | GET | Trial Balance |
/Reports/BankSummary | GET | Bank account summary |
/Reports/BudgetSummary | GET | Budget vs actual |
/Reports/ExecutiveSummary | GET | Executive overview |
/Reports/TenNinetyNine | GET | 1099 report (US) |
periods parameter for trend analysispaymentsOnly=true for cash flow analysis~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.