neekey's blog


Python Xero Integration Setup

I got a task about Xero integration in my work last week, aiming to copy invoices from one account to the other. To perform this task, the workflow will be:

    1. Invoice created/updated in Xero account A
    1. The webhook from Xero account A is triggered
    1. Our App handles the webhook and copy the newly created/updated invoice to Xero account B

This article will show you how to setup Xero and talk about some tricky parts of the task.

Set Up Xero Integration

To get started, a xero account is needed. Just signup and Xero will prompt to create an organization. Create a trial organization and a demo so that invoices can be copied from one to the other.

Private Application

To get access to data of an Xero account, an Application is required, in our case, the application is only used by the organization not shared by others, so a private application is the answer.

Not like a public application, a private application is associated with an organization, and can only be used by that organization.

To setup an application for our organization, go to Xero Developer and click the “New App” button:

Input app name and select with organization the application will be associated to.

Create Public/Private Key Pair

The rest of the form is about setting up the credential to get access to the application. Xero has documentation to help you set it up.

A Public/Private key pair will be created at the end, upload the public key to Xero (and keep the private key file):

Click the “Create App”. Customer key will be found in the App detail screen, it will be used later:

Go through the same process to create another application for the other organization (since there are two organizations).

Setup Webhook

Go to the webhook setting page in your App:

Once the webhook is saved, the “webhook key” will be present and the status will show as Intent To Receive required, which indicate at this moment, the webhook is not ready to use. To enable the webhook, Xero requires the application to pass its verification. It does the process by:

  • send a dummy webhook request to your server with a special header called X-Xero-Signature
  • the server needs to encrypt the whole request body and the webhook key together, and based on whether the encrypted result is the same as the value of the X-Xero-Signature, the server needs to response differently:
    • if the same, return 200 with an empty body
    • if not, return 403

Create API Endpoint to Handle Webhook

Since the process of the webhook verification is clear, an API endpoint can be built as bellow:

from app.api.resources import BaseResource
from flask import current_app, request, Response
from app.xero import verify_xero_webhook_request_for_uae

class XeroWebhookResource(BaseResource):
    def post(self):
        if verify_xero_webhook_request_for_uae(request):
                f'Xero webhook received and passed the verification'
            # ... perform the task
            return Response(
                f'Xero webhook received and failed the verification'
            return Response(

Once the endpoint is setup, get back to the webhook page, hit the “send the ITR” button, the verification process will start. Xero will make several dummy requests with true of false value of header X-Xero-Signature to test if your server can handle them correctly, if all pass, the status will turn to a blue OK.

In addition, to make a remote webhook server work with local development server, Ngrok is highly recommended.

Setup Xero Integraion with Python

Now then we can focus on the task itself.

There’s a third party library pyxero we can use to do the API calling and authentication work.

Follow the Private Application, to set up a Xero client, two things are needed:

  • Application Consumer Key, which can be found in your App detail page
  • Private RSA File, which is the private key file created in the App setup step.

Once you get these two file ready, follow the code below:

from xero import Xero
from xero.auth import PrivateCredentials
with open(<path to rsa key file>) as keyfile:
    rsa_key =
credentials = PrivateCredentials(<consumer_key>, rsa_key)
xero = Xero(credentials)

The Xero client can then be used:


In production environment, instead of saving the private key into the code base, uploading it into a private storage service (for example, Amazon S3) that your machine have the access to will be a good idea. In my case, I dymatically download the file whenever the Xero client is needed:

def download_rsa_file_from_s3():
    s3 = boto3.resource('s3')
    bucket_name = current_app.config['SECRET_KEY_S3_BUCKET']
    bucket = s3.Bucket(bucket_name)
    rsa_file_path = get_rsa_file_path()
    if path.exists(rsa_file_path) is False:
            f'Download Xero private RSA key'
            f' from {bucket_name}/{PRIVATE_RSA_S3_FILE_PATH}'
            f' to {rsa_file_path}')
        bucket.download_file(PRIVATE_RSA_S3_FILE_PATH, rsa_file_path)
    return rsa_file_path

Handle Webhook Events

The body of each Xero webhook event is like below:

        "eventType": "CREATE",
        "eventCategory": "INVOICE",
        "resourceId": "xxxxxxx"

So to get the target invoice that has just been created:

invoice_id = event.get("resourceId")
invoice = xero.invoices.get(invoice_id)

Copy Invoice to Another Account

To make the copy operation successful, it’s not just as easy as using a create API against the target account, other requirements are needed:

  • the line items exist with the same item code in target account
  • the contract exist or not (will affect the parameters to create/update the invoice)
  • credit notes need to be manually handled

The create API is:


The update API is:

The structures of new_invoice and updated_invoice are quite the same as what the response of a GET API, you can check the details doc here, but to make the copy work, we need to tweak the structure a bit.

Update Line Items

One thing needs to be made sure is the item codes exist in both account. After that, you need to remove LineItemID, TaxType and TaxAmount from the line items:

# update line items
                remove_tax_rate_from_line_items(invoice.get('LineItems', []))

Update Contract

If an contract exists, only the ContactID is needed to pass to the API:

# check if the contact exists
        existing_contact = target_xero.contacts.filter(Name=contact_name)
        if len(existing_contact) > 0:
                f'Xero webhook invoice tranfer: contact {contact_name} already exists'
            existing_contact = existing_contact[0]
                Contact={'ContactID': existing_contact.get('ContactID')}

You can see here it uses Name as the filter to find Contact, since in Xero, the name for contract is unique.

Credit Notes

The credit notes will not be automatically copied through the APIs, a manual creation and allocation is required.

Before credit notes, the invoice should be created or updated first and the credit Note should be removed from the parameters.

invoice.pop('CreditNotes', None)

After the update or create of the invoice finishs, create the credit notes and allocate to the invoice:

# create credit note
new_credit_note = create_credit_note(
# allocate the credit to target invoice
        ) for allocation in credit_note_item[0].get('Allocations')

One hack here is that PyXero does not support Allocation API, a work around here needs to be use:

def allocate_credit_note_payment(xero, credit_note_guid, allocations):
    Must pass in xero as it needs credentials if using public method.
    allocations should be an array of dictionaries containing amount,
     invoice:invoice idXero GUIDs for the contacts.
    The request should look like:
    PUT CreditNotes/a1665e41-719b-400d-ae59-bc92655d8366/Allocations
    # Store the original endpoint base_url
    old_base_url = xero.creditnotes.base_url
    old_name =
    old_singular = xero.creditnotes.singular
    result = None
    # Call the API
            = '{}/CreditNotes/{}'.format(old_base_url, credit_note_guid) = 'Allocations'
        xero.creditnotes.singular = 'Allocation'
        result = xero.creditnotes.put(allocations)
    except Exception as e:
        raise e
        # Reset the base_url
        xero.creditnotes.base_url = old_base_url = old_name
        xero.creditnotes.singular = old_singular
    return result

Done. Hope it helps.

Tools & Services I Use

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.