Breadwinner for Xero Apex Guide

Salesforce to Xero Integration via Apex

A developer guide to connecting Salesforce and Xero using Apex code — covering OAuth authentication, contact sync, and invoice operations.

Integration Overview

Salesforce provides powerful tools for integrating with external systems like Xero. Using Apex HTTP callouts, you can authenticate with Xero's API, read and write contacts, and create or update invoices — all from within Salesforce.

Before building this yourself: Breadwinner for Xero is a native Salesforce integration that takes minutes to set up, handles OAuth, error recovery, and ongoing API changes automatically, and is trusted by hundreds of Salesforce customers with a 4.93 AppExchange rating. Most developers who build a custom Apex integration end up migrating to Breadwinner within months once the maintenance burden becomes clear. Salesforce Xero Integration →

This guide walks through the key building blocks. You'll need admin access to both Salesforce and Xero, plus experience writing Apex.

1. Remote Site Settings

Before Salesforce can make callouts to Xero, you need to register two endpoints in Setup → Remote Site Settings:

  • Authentication https://identity.xero.com/
  • Data Operations https://api.xero.com/

2. OAuth 2.0 Authentication

Create an app at the Xero Developer Portal to obtain your Client ID and Client Secret. These credentials are used to generate access tokens via the OAuth 2.0 client credentials flow. Tokens are valid for 1800 seconds (30 minutes).

XeroAuth.cls
public class XeroAuth {
    public static String getAccessToken() {
        String clientId = 'YOUR_CLIENT_ID';
        String clientSecret = 'YOUR_CLIENT_SECRET';
        Blob headerValue = Blob.valueOf(clientId + ':' + clientSecret);
        String authorizationHeader = 'Basic ' +
            EncodingUtil.base64Encode(headerValue);
        String payload = 'grant_type=client_credentials' +
            '&scope=accounting.transactions accounting.contacts';

        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://identity.xero.com/connect/token');
        req.setMethod('POST');
        req.setHeader('Authorization', authorizationHeader);
        req.setHeader('Content-Type',
            'application/x-www-form-urlencoded');
        req.setBody(payload);

        Http http = new Http();
        HTTPResponse res = http.send(req);

        if (res.getStatusCode() == 200 && res.getBody() != null) {
            Map<String, Object> respMap =
                (Map<String, Object>) JSON.deserializeUntyped(
                    res.getBody()
                );
            if (respMap.containsKey('access_token'))
                return String.valueOf(
                    respMap.get('access_token')
                );
        }

        return null;
    }
}

3. Working with Xero Contacts

Reading Contacts

Retrieve all contacts from Xero, or fetch a specific contact by appending its ID to the endpoint.

XeroContact.cls — Read
public class XeroContact {
    public static final String apiEndpoint =
        'https://api.xero.com/api.xro/2.0/';

    public static String getAllXeroContacts() {
        String accessToken = XeroAuth.getAccessToken();

        if (accessToken != null) {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(apiEndpoint + 'Contacts');
            req.setMethod('GET');
            req.setHeader('Authorization',
                'Bearer ' + accessToken);

            Http http = new Http();
            HTTPResponse res = http.send(req);

            if (res.getStatusCode() == 200
                && res.getBody() != null) {
                return res.getBody();
            }
        }
        return null;
    }
}

Creating & Updating Contacts

A single method handles both creation (PUT) and updates (POST) based on the isCreate parameter.

XeroContact.cls — Create/Update
public static String createUpdateXeroContact(
    Boolean isCreate
) {
    String accessToken = XeroAuth.getAccessToken();

    if (accessToken != null) {
        String jsonBody = '{'
            + '"Contacts": [{'
            + '  "Name": "Bruce Banner",'
            + '  "EmailAddress": "hulk@avengers.com",'
            + '  "Phones": [{'
            + '    "PhoneType": "MOBILE",'
            + '    "PhoneNumber": "555-1212",'
            + '    "PhoneAreaCode": "415"'
            + '  }],'
            + '  "PaymentTerms": {'
            + '    "Bills": {'
            + '      "Day": 15,'
            + '      "Type": "OFCURRENTMONTH"'
            + '    },'
            + '    "Sales": {'
            + '      "Day": 10,'
            + '      "Type": "DAYSAFTERBILLMONTH"'
            + '    }'
            + '  }'
            + '}]}';

        HttpRequest req = new HttpRequest();
        req.setEndpoint(apiEndpoint + 'Contacts');
        req.setMethod(isCreate ? 'PUT' : 'POST');
        req.setHeader('Authorization',
            'Bearer ' + accessToken);
        req.setHeader('Content-Type',
            'application/json');
        req.setHeader('Accept', 'application/json');
        req.setBody(jsonBody);

        Http http = new Http();
        HTTPResponse res = http.send(req);

        if (res.getStatusCode() == 200
            && res.getBody() != null) {
            return res.getBody();
        }
    }
    return null;
}

4. Working with Xero Invoices

Reading Invoices

Retrieve all invoices from Xero, or filter by a specific invoice ID.

XeroInvoice.cls — Read
public class XeroInvoice {
    public static final String apiEndpoint =
        'https://api.xero.com/api.xro/2.0/';

    public static String getAllXeroInvoices() {
        String accessToken = XeroAuth.getAccessToken();

        if (accessToken != null) {
            HttpRequest req = new HttpRequest();
            req.setEndpoint(apiEndpoint + 'Invoices');
            req.setMethod('GET');
            req.setHeader('Authorization',
                'Bearer ' + accessToken);

            Http http = new Http();
            HTTPResponse res = http.send(req);

            if (res.getStatusCode() == 200
                && res.getBody() != null) {
                return res.getBody();
            }
        }
        return null;
    }
}

Creating & Updating Invoices

Similar to contacts — use PUT to create and POST to update. You'll need a valid ContactID and line item details.

XeroInvoice.cls — Create/Update
public static String createUpdateXeroInvoice(
    Boolean isCreate
) {
    String accessToken = XeroAuth.getAccessToken();

    if (accessToken != null) {
        String jsonBody = '{'
            + '"Invoices": [{'
            + '  "Type": "ACCREC",'
            + '  "Contact": {'
            + '    "ContactID": "YOUR_CONTACT_ID"'
            + '  },'
            + '  "LineItems": [{'
            + '    "Description": "Acme Tires",'
            + '    "Quantity": 2,'
            + '    "UnitAmount": 20,'
            + '    "AccountCode": "200",'
            + '    "TaxType": "NONE",'
            + '    "LineAmount": 40'
            + '  }],'
            + '  "Date": "2024-03-11",'
            + '  "DueDate": "2024-04-10",'
            + '  "Reference": "Website Design",'
            + '  "Status": "AUTHORISED"'
            + '}]}';

        HttpRequest req = new HttpRequest();
        req.setEndpoint(apiEndpoint + 'Invoices');
        req.setMethod(isCreate ? 'PUT' : 'POST');
        req.setHeader('Authorization',
            'Bearer ' + accessToken);
        req.setHeader('Content-Type',
            'application/json');
        req.setHeader('Accept', 'application/json');
        req.setBody(jsonBody);

        Http http = new Http();
        HTTPResponse res = http.send(req);

        if (res.getStatusCode() == 200
            && res.getBody() != null) {
            return res.getBody();
        }
    }
    return null;
}

What Custom Apex Doesn't Cover

The code above gets you started, but a production-grade integration needs significantly more. Building and maintaining all of this yourself takes weeks of development and ongoing engineering time:

  • 1 Error handling & retry logic — API rate limits, timeouts, partial failures, and network issues
  • 2 Race conditions — handling concurrent updates from both Salesforce and Xero users
  • 3 UI components — Lightning Web Components for viewing and managing Xero data inside Salesforce
  • 4 Token refresh & credential management — secure storage and automatic renewal of OAuth tokens
  • 5 Ongoing maintenance — keeping up with Xero API changes, Salesforce releases, and security patches
  • 6 Multi-currency, tax codes, and credit notes — the long tail of accounting complexity

Skip the build. Use Breadwinner.

Breadwinner for Xero is a native Salesforce integration that handles all of the above — and more. Install from the AppExchange, configure in minutes, and start syncing Xero data with Salesforce the same day. No custom Apex required.

  • Pre-built Lightning Web Components for invoices, contacts, and payments
  • Full API and Apex Generator for custom extensions
  • Error handling, retry logic, and race condition management built in
  • SOC 2 Type II compliant, continuously maintained

Building a different integration? See our QuickBooks to Salesforce Apex guide.

"Breadwinner allows Salesforce and Xero to communicate which has eliminated a huge amount of manual data work each month."

— Port & Starboard · Read the full story

Built the integration and finding it harder to maintain than expected? Breadwinner for Xero replaces custom Apex integrations with a fully managed, native Salesforce solution. Salesforce Xero Integration — Learn more →