From xero
Use this skill when working with Xero contacts (customers/suppliers) - creating, searching, updating, and managing client organizations. Covers contact fields, contact groups, MSP client management, billing address handling, and cross-referencing with PSA systems.
How this skill is triggered — by the user, by Claude, or both
Slash command
/xero:contactsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Contacts are the foundational entity in Xero, representing customers (clients you invoice), suppliers (vendors you pay), or both. Every invoice, payment, credit note, and bank transaction is linked to a contact. For MSPs, contacts typically represent managed services clients, hardware vendors, software suppliers, and subcontractors.
Contacts are the foundational entity in Xero, representing customers (clients you invoice), suppliers (vendors you pay), or both. Every invoice, payment, credit note, and bank transaction is linked to a contact. For MSPs, contacts typically represent managed services clients, hardware vendors, software suppliers, and subcontractors.
Xero contacts can be customers, suppliers, or both. The type is determined by usage rather than a fixed field:
| Role | Description | MSP Example |
|---|---|---|
| Customer | Contacts you create sales invoices (ACCREC) for | Managed services clients |
| Supplier | Contacts you receive bills (ACCPAY) from | Software vendors, ISPs |
| Customer & Supplier | Both roles | Partner MSPs, distributors |
| Status | Description |
|---|---|
ACTIVE | Active contact (default) |
ARCHIVED | Archived contact (hidden from lists) |
GDPR_REQUEST | GDPR deletion requested |
Contacts can be organized into groups for reporting and filtering:
| Group | MSP Use Case |
|---|---|
| Managed Services | Clients on monthly contracts |
| Break-Fix | Ad-hoc support clients |
| Vendors | Hardware and software suppliers |
| Partners | Co-managed or referral partners |
| Field | Type | Required | Description |
|---|---|---|---|
ContactID | string (UUID) | System | Auto-generated unique identifier |
Name | string | Yes | Contact/company name (must be unique) |
ContactNumber | string | No | Your reference number for this contact |
AccountNumber | string | No | Account number in your system |
ContactStatus | string | No | ACTIVE, ARCHIVED, or GDPR_REQUEST |
EmailAddress | string | No | Primary email address |
FirstName | string | No | First name (for individual contacts) |
LastName | string | No | Last name (for individual contacts) |
BankAccountDetails | string | No | Bank account information |
TaxNumber | string | No | Tax identification number |
AccountsReceivableTaxType | string | No | Default tax type for sales |
AccountsPayableTaxType | string | No | Default tax type for purchases |
DefaultCurrency | string | No | Default currency code (e.g., USD, AUD) |
IsSupplier | boolean | Read-only | Has supplier invoices |
IsCustomer | boolean | Read-only | Has customer invoices |
Contacts support two address types: POBOX (mailing) and STREET (physical):
| Field | Type | Description |
|---|---|---|
AddressType | string | POBOX or STREET |
AddressLine1 | string | Street address line 1 |
AddressLine2 | string | Street address line 2 |
AddressLine3 | string | Street address line 3 |
AddressLine4 | string | Street address line 4 |
City | string | City |
Region | string | State/province/region |
PostalCode | string | Postal/zip code |
Country | string | Country |
AttentionTo | string | Attention to name |
Contacts support four phone types: DEFAULT, DDI, MOBILE, FAX:
| Field | Type | Description |
|---|---|---|
PhoneType | string | DEFAULT, DDI, MOBILE, or FAX |
PhoneNumber | string | Phone number |
PhoneAreaCode | string | Area code |
PhoneCountryCode | string | Country code |
| Field | Type | Description |
|---|---|---|
Balances.AccountsReceivable.Outstanding | decimal | Total outstanding AR |
Balances.AccountsReceivable.Overdue | decimal | Total overdue AR |
Balances.AccountsPayable.Outstanding | decimal | Total outstanding AP |
Balances.AccountsPayable.Overdue | decimal | Total overdue AP |
UpdatedDateUTC | datetime | Last modification timestamp |
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
With Filters:
# Search by name (partial match)
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts?where=Name.Contains(%22Acme%22)" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# Active customers only
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts?where=ContactStatus==%22ACTIVE%22&&IsCustomer==true" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# With pagination
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts?page=1" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts/${CONTACT_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
curl -s -X POST "https://api.xero.com/api.xro/2.0/Contacts" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Content-Type: application/json" \
-d '{
"Name": "Acme Corp",
"ContactNumber": "MSP-001",
"AccountNumber": "ACME001",
"EmailAddress": "[email protected]",
"Addresses": [
{
"AddressType": "STREET",
"AddressLine1": "123 Main Street",
"City": "Springfield",
"Region": "IL",
"PostalCode": "62704",
"Country": "US"
},
{
"AddressType": "POBOX",
"AddressLine1": "PO Box 456",
"City": "Springfield",
"Region": "IL",
"PostalCode": "62704",
"Country": "US"
}
],
"Phones": [
{
"PhoneType": "DEFAULT",
"PhoneNumber": "555-0123",
"PhoneAreaCode": "217"
}
],
"DefaultCurrency": "USD"
}'
curl -s -X POST "https://api.xero.com/api.xro/2.0/Contacts/${CONTACT_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Content-Type: application/json" \
-d '{
"ContactID": "'${CONTACT_ID}'",
"EmailAddress": "[email protected]",
"Phones": [
{
"PhoneType": "DEFAULT",
"PhoneNumber": "555-9999",
"PhoneAreaCode": "217"
}
]
}'
curl -s -X POST "https://api.xero.com/api.xro/2.0/Contacts/${CONTACT_ID}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Content-Type: application/json" \
-d '{
"ContactID": "'${CONTACT_ID}'",
"ContactStatus": "ARCHIVED"
}'
# Search by name
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts?where=Name.StartsWith(%22Acme%22)" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# Search by email
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts?where=EmailAddress==%[email protected]%22" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
# Search by account number
curl -s -X GET "https://api.xero.com/api.xro/2.0/Contacts?where=AccountNumber==%22ACME001%22" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "xero-tenant-id: ${XERO_TENANT_ID}" \
-H "Accept: application/json"
async function onboardMspClient(clientData) {
const token = await auth.getToken();
const contact = await createContact(token, {
Name: clientData.companyName,
ContactNumber: clientData.psaId,
AccountNumber: clientData.accountCode,
EmailAddress: clientData.billingEmail,
Addresses: [
{
AddressType: 'STREET',
AddressLine1: clientData.address,
City: clientData.city,
Region: clientData.state,
PostalCode: clientData.zip,
Country: clientData.country
}
],
Phones: [
{
PhoneType: 'DEFAULT',
PhoneNumber: clientData.phone
}
],
DefaultCurrency: clientData.currency || 'USD'
});
return contact;
}
async function offboardClient(contactId) {
const token = await auth.getToken();
// Check outstanding balance
const contact = await getContact(token, contactId);
const outstanding = contact.Balances?.AccountsReceivable?.Outstanding || 0;
if (outstanding > 0) {
console.log(`WARNING: ${contact.Name} has $${outstanding} outstanding.`);
return { status: 'blocked', reason: 'outstanding_balance', amount: outstanding };
}
// Archive the contact
await updateContact(token, contactId, { ContactStatus: 'ARCHIVED' });
return { status: 'archived', contact: contact.Name };
}
Use the ContactNumber field to store your PSA system's client ID:
async function findByPsaId(psaId) {
const token = await auth.getToken();
const where = encodeURIComponent(`ContactNumber=="${psaId}"`);
const response = await fetch(
`https://api.xero.com/api.xro/2.0/Contacts?where=${where}`,
{
headers: {
'Authorization': `Bearer ${token}`,
'xero-tenant-id': process.env.XERO_TENANT_ID,
'Accept': 'application/json'
}
}
);
const data = await response.json();
return data.Contacts?.[0] || null;
}
async function generateClientReport() {
const contacts = await fetchAllContacts();
return contacts
.filter(c => c.IsCustomer && c.ContactStatus === 'ACTIVE')
.map(contact => ({
name: contact.Name,
accountNumber: contact.AccountNumber,
email: contact.EmailAddress,
outstanding: contact.Balances?.AccountsReceivable?.Outstanding || 0,
overdue: contact.Balances?.AccountsReceivable?.Overdue || 0,
city: contact.Addresses?.find(a => a.AddressType === 'STREET')?.City
}))
.sort((a, b) => b.overdue - a.overdue);
}
| Code | Message | Resolution |
|---|---|---|
| 400 | Name must be unique | Use a different contact name |
| 400 | Name is required | Provide Name field |
| 401 | Unauthorized | Refresh access token |
| 403 | Forbidden | Check tenant ID and OAuth scopes |
| 404 | Contact not found | Verify ContactID |
| 429 | Rate limit exceeded | Wait and retry |
| Error | Cause | Fix |
|---|---|---|
| Name must be unique | Duplicate contact name | Use unique name or find existing |
| Name is required | Missing Name field | Add Name to request body |
| Invalid email | Malformed email address | Fix email format |
| Invalid phone | Malformed phone number | Fix phone format |
async function safeCreateContact(data) {
try {
return await createContact(data);
} catch (error) {
if (error.message?.includes('must be unique')) {
// Contact exists - find and return it
const existing = await searchContactByName(data.Name);
return existing;
}
if (error.status === 401) {
// Token expired - refresh and retry
await auth.refreshToken();
return await createContact(data);
}
throw error;
}
}
| Endpoint | Method | Description |
|---|---|---|
/Contacts | GET | List contacts (paginated, filterable) |
/Contacts | POST | Create or update contacts |
/Contacts/{ContactID} | GET | Get single contact |
/Contacts/{ContactID} | POST | Update a contact |
/Contacts/{ContactID}/Attachments | GET | List contact attachments |
/ContactGroups | GET | List contact groups |
/ContactGroups | POST | Create contact group |
/ContactGroups/{GroupID}/Contacts | PUT | Add contacts to group |
/ContactGroups/{GroupID}/Contacts/{ContactID} | DELETE | Remove contact from group |
npx claudepluginhub p/drewrox2009-xero-msp-claude-plugins-xero-xeroGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Provides Slack GIF creation utilities with dimension/FPS/color constraints and Python PIL-based frame generation. Use for animated Slack emoji or message GIFs.