Xero Accounts — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Xero Accounts (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.
The chart of accounts (COA) in Xero defines the general ledger structure for your organization. Every invoice line item, payment, and bank transaction references an account code. For MSPs, a well-structured COA enables tracking revenue by service line (managed services, projects, hardware sales), expenses by vendor category, and provides the foundation for meaningful financial reporting.
ledger code; the organization you bill is xero-contacts.
structure only and carries no figures; use xero-reports.
xero-api-patterns.Xero organizes accounts into five standard accounting classes:
| Class | Description | MSP Examples |
|---|---|---|
ASSET | Things you own | Bank accounts, accounts receivable, equipment |
EQUITY | Owner's stake | Retained earnings, owner's equity |
EXPENSE | Costs of business | Software licenses, salaries, ISP costs |
LIABILITY | Things you owe | Accounts payable, loans, tax liabilities |
REVENUE | Income earned | Managed services, project revenue, hardware sales |
Each class contains specific account types. The ones that matter most for MSP work: BANK (the only type payments can be applied to), REVENUE/SALES, DIRECTCOSTS (cost of goods sold), and OVERHEADS/EXPENSE.
See references/fields.md for the complete field reference, the full account-type table, and the list of Xero-managed system accounts.
A typical MSP chart of accounts includes:
Revenue (200-299)
200 - Managed Services Revenue
210 - Project Revenue
220 - Hardware Sales
230 - Software License Revenue
240 - Cloud Services Revenue
250 - Consulting Revenue
Cost of Sales (400-499)
400 - Software License Costs
410 - Hardware Costs
420 - Cloud Platform Costs (Azure, AWS)
430 - ISP/Connectivity Costs
440 - Subcontractor Costs
Expenses (500-699)
500 - Salaries & Wages
510 - Employee Benefits
520 - Office Rent
530 - Insurance
540 - Marketing
550 - Professional Development
560 - Tools & SubscriptionsEvery request needs both Authorization: Bearer ${ACCESS_TOKEN} and xero-tenant-id: ${XERO_TENANT_ID}. Two Xero-specific quirks apply here:
/Accounts/{AccountID}, and the body must repeat the AccountID.
string literals:
# Revenue accounts only
curl -s -X GET "https://api.xero.com/api.xro/2.0/Accounts?where=Class==%22REVENUE%22" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"See references/api.md for the full endpoint catalog and create/update/archive/delete examples.
async function setupMspRevenueAccounts() {
const accounts = [
{ Code: '200', Name: 'Managed Services Revenue', Type: 'REVENUE', Description: 'Monthly recurring managed services contracts' },
{ Code: '210', Name: 'Project Revenue', Type: 'REVENUE', Description: 'One-time project and implementation work' },
{ Code: '220', Name: 'Hardware Sales', Type: 'REVENUE', Description: 'Hardware sales and procurement markup' },
{ Code: '230', Name: 'Software License Revenue', Type: 'REVENUE', Description: 'Software license resale (M365, security, etc.)' },
{ Code: '240', Name: 'Cloud Services Revenue', Type: 'REVENUE', Description: 'Cloud hosting and IaaS/PaaS resale' },
{ Code: '250', Name: 'Consulting Revenue', Type: 'REVENUE', Description: 'Ad-hoc consulting and advisory services' }
];
const results = [];
for (const account of accounts) {
try {
const result = await createAccount(account);
results.push({ code: account.Code, status: 'created' });
} catch (error) {
results.push({ code: account.Code, status: 'error', message: error.message });
}
}
return results;
}async function validateAccountCodes(lineItems) {
const accounts = await fetchAllAccounts();
const accountCodes = new Set(accounts.map(a => a.Code));
const revenueAccounts = new Set(
accounts.filter(a => a.Class === 'REVENUE').map(a => a.Code)
);
const issues = [];
for (const item of lineItems) {
if (!accountCodes.has(item.AccountCode)) {
issues.push(`Account code '${item.AccountCode}' does not exist`);
} else if (!revenueAccounts.has(item.AccountCode)) {
issues.push(`Account code '${item.AccountCode}' is not a revenue account`);
}
}
return { valid: issues.length === 0, issues };
}async function getRevenueBreakdown(startDate, endDate) {
const accounts = await fetchAllAccounts();
const revenueAccounts = accounts.filter(a => a.Class === 'REVENUE');
const invoices = await fetchAllInvoices({
where: `Type=="ACCREC"&&Status!="VOIDED"&&Status!="DELETED"&&Date>=DateTime(${startDate})&&Date<=DateTime(${endDate})`
});
const breakdown = {};
for (const account of revenueAccounts) {
breakdown[account.Code] = {
name: account.Name,
total: 0,
invoiceCount: 0
};
}
for (const invoice of invoices) {
for (const line of invoice.LineItems || []) {
if (breakdown[line.AccountCode]) {
breakdown[line.AccountCode].total += line.LineAmount || 0;
breakdown[line.AccountCode].invoiceCount++;
}
}
}
return breakdown;
}async function getBankAccounts() {
const accounts = await fetchAllAccounts();
return accounts
.filter(a => a.Type === 'BANK' && a.Status === 'ACTIVE')
.map(a => ({
accountId: a.AccountID,
code: a.Code,
name: a.Name,
bankAccountNumber: a.BankAccountNumber,
currencyCode: a.CurrencyCode
}));
}archive the account instead (Status: "ARCHIVED").
DEBTORS, CREDITORS, GST, ROUNDINGand friends are Xero-managed and reject modification.
accounts, so a "code already exists" error may point at an archived record.
See references/errors.md for the complete error-code table.
async function safeCreateAccount(data) {
try {
return await createAccount(data);
} catch (error) {
if (error.message?.includes('already exists')) {
// Account exists - find and return it
const accounts = await fetchAllAccounts();
return accounts.find(a => a.Code === data.Code);
}
throw error;
}
}~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.