Xero API integration with Python
By manhnv, at: 12:00 Ngày 23 tháng 7 năm 2024
Leveraging Python for Xero API: Automating Financial Processes
Introduction
Welcome everyone to another article in our series, Leveraging Python for API(s). In this installment, we will explore how to use Python to interact with the Xero API for invoice management. This guide covers the basics of making API calls to Xero with Python, demonstrating through a real-world case study how this approach can streamline your invoice management processes.
Imagine you are a business owner using Xero for accounting. You need to update specific details on many invoices regularly. Manually making these updates through Xero's UI is time-consuming and tedious. Python and the Xero API can automate this task, saving you time and reducing errors.
In this guide, you will learn how to set up your environment, authenticate with the Xero API, fetch invoices, and update them. We will also cover error handling and best practices for structuring your code.
By the end of this guide, you'll have a solid understanding of how to use Python to interact with the Xero API and apply this knowledge to your own business needs. Let's dive in and start automating your invoice management!
Xero API Overview
The Xero API is a RESTful web service that uses OAuth 2.0 for authentication and authorization. It allows third-party applications to interact with the Xero platform, enabling various tasks such as querying data, posting transactions, and more…
Key Endpoints:
- Invoices:
- Endpoint:
/api.xro/2.0/Invoices
- Methods:
GET
,POST
,PUT
- Description: Allows for the retrieval, creation, and updating of invoices within Xero. This is essential for managing your accounts receivable and ensuring accurate financial records.
- Ref: https://developer.xero.com/documentation/api/accounting/invoices
- Endpoint:
- Contacts:
- Endpoint:
/api.xro/2.0/Contacts
- Methods:
GET
,POST
,PUT
- Description: Enables access to contact information, facilitating the management of customer and supplier details. This is crucial for maintaining up-to-date and accurate contact lists.
- Ref: https://developer.xero.com/documentation/api/accounting/contacts
- Endpoint:
- Payments:
- Endpoint:
/api.xro/2.0/Payments
- Methods:
GET
,POST
,PUT
- Description: Provides functionality to handle payments against invoices, helping you keep track of incoming and outgoing payments, and ensuring your financial transactions are accurately recorded.
- Ref: https://developer.xero.com/documentation/api/accounting/payments
- Endpoint:
Setting Up the Environment
First, set up your development environment with the necessary packages.
NOTE: in order to follow the guide eazily, you should have your system installed python and pyenv first.
# create the virtual environment for development
pyenv virtualenv 3.10.8 python_xero # you can change 3.10.8 with your installed version of Python
pyenv activate python_xero
# install neccessary dependencies
pip install requests # library for making HTTP(S) request in python
pip install ipython # an interactive shell, alternate to default python shell, much more useful
Now , after all the process are done, you can now access the new shell with command:
ipython
Authenticating with the Xero API
To make API calls, you'll need to authenticate with Xero using OAuth 2.0. Follow the steps in the Xero OAuth Documentation to obtain your access tokens.
Or you can copy the code below to quickly obtain your new access token, the explaination for each step also available in the code.
NOTE: you will need to get your Xero’s client id and client secret first, which can be obtained in this app management page.
Go to this link, click on your app (your app must be the Custom Connection type, not other type of app, follow this link for more information), then click on the Configuration section, then you can see your Client ID. For Client Secret, you will need to create one, by simply click on Create.
For more information about those credentials, please follow this link.
Note that you should not share your credentials to any one, because with that credentials they can have full access to your Xero site.
import base64
import requests
from django.conf import settings
# Your Xero app credentials
client_id = "<your app="" client="" id="">"
client_secret = "<your app="" client="" secret="">"
# Xero token endpoint
token_url = '<https: connect="" identity.xero.com="" token="">'
# Encode client_id and client_secret
credentials = f'{client_id}:{client_secret}'
encoded_credentials = base64.b64encode(credentials.encode()).decode()
# Headers
headers = {
'Authorization': f'Basic {encoded_credentials}',
'Content-Type': 'application/x-www-form-urlencoded'
}
# Request body
data = {
'grant_type': 'client_credentials',
'scope': 'accounting.transactions accounting.contacts accounting.settings' # the scopes that you want to grant, space-separated list of scopes
}
# Make the API call
response = requests.post(token_url, headers=headers, data=data)
# Check if the request was successful
if response.status_code == 200:
# Parse the JSON response
token_data = response.json()
access_token = token_data['access_token']
print(f"Access Token: {access_token}")
else:
print(f"Error: {response.status_code}")
print(response.text)</https:></your></your>
Fetching Invoices
Use the Invoice API to fetch invoice(s) from your Xero account.
To do this, you’ll need to know the id of the invoice that you want to get, or skip it if you want to get all the invoice. In my case, I want to fetch an specific invoice, so that I need that invoice’s id.
# fetching invoice code
import requests # import the requests module, required to make API calls
invoice_url = "<https: 2.0="" api.xero.com="" api.xro="" invoices="">" # base invoice url from Xero
access_token = "<your access="" token="">" # your Access Token returned from the Token API call
invoice_id = "YOUR INVOICE ID" # the ID of the invoice that you want to fetch
# url to fetch the invoice
url = f"{invoice_url}{invoice_id}"
# prepare the headers for the API call
headers = {
"Authorization": f"Bearer {access_token}", # required for authentication
"Accept": "application/json", # specify the type of data you expected to accept from response
}
# make the request
resp = requests.get(url, headers=headers)
# check the response status code, if it is 200 then it was successful
if resp.status_code == 200:
resp_data = resp.json()
# we need to check again the status of the response
# sometimes, the Xero API return 200 for the status code but
# the content of the response is an error message
if resp_data["Status"] != "OK":
raise Exception(f"Failed to request invoice. {resp_data}")
invoices = resp_data["Invoices"]
# because the API returns a list of invoices, eventhough we only use an
# invoice ID to fetch, so that we need to print out the first item in the list
print(invoices[0])</your></https:>
After running this code, in success, you can see that the following content is printed into your shell (the below is my invoice with ID fee88eea-f2aa-4a71-a372-33d6d83d3c45
):
{
"Type": "ACCREC",
"InvoiceID": "fee88eea-f2aa-4a71-a372-33d6d83d3c45",
"InvoiceNumber": "INV-0027",
"Reference": "Ref MK815",
"Prepayments": [],
"Overpayments": [],
"AmountDue": 396.0,
"AmountPaid": 0.0,
"SentToContact": false,
"CurrencyRate": 1.0,
"IsDiscounted": false,
"HasAttachments": false,
"HasErrors": false,
"Attachments": [],
"InvoicePaymentServices": [],
"Contact": {
"ContactID": "5b96e86b-418e-48e8-8949-308c14aec278",
"ContactStatus": "ACTIVE",
"Name": "Marine Systems",
"EmailAddress": "",
"BankAccountDetails": "",
"Addresses": [
{
"AddressType": "POBOX",
"City": "",
"Region": "",
"PostalCode": "",
"Country": ""
},
{
"AddressType": "STREET",
"City": "",
"Region": "",
"PostalCode": "",
"Country": ""
}
],
"Phones": [
{
"PhoneType": "FAX",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": ""
},
{
"PhoneType": "MOBILE",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": ""
},
{
"PhoneType": "DDI",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": ""
},
{
"PhoneType": "DEFAULT",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": ""
}
],
"UpdatedDateUTC": "/Date(1721153021917+0000)/",
"ContactGroups": [],
"IsSupplier": false,
"IsCustomer": true,
"DefaultCurrency": "USD",
"SalesTrackingCategories": [],
"PurchasesTrackingCategories": [],
"ContactPersons": [],
"HasValidationErrors": false
},
"DateString": "2024-07-15T00:00:00",
"Date": "/Date(1721001600000+0000)/",
"DueDateString": "2024-07-21T00:00:00",
"DueDate": "/Date(1721520000000+0000)/",
"BrandingThemeID": "d613f7f9-8fcb-477f-97f0-31eb85b7e5cf",
"Status": "AUTHORISED",
"LineAmountTypes": "Inclusive",
"LineItems": [
{
"Description": "Marketing guides",
"UnitAmount": 99.0,
"TaxType": "OUTPUT",
"TaxAmount": 30.18,
"LineAmount": 396.0,
"AccountCode": "200",
"Tracking": [],
"Quantity": 4.0,
"LineItemID": "8dd05881-be4a-4765-b95e-cce0d7395f9f",
"AccountID": "d1ebb97b-d207-4ccb-9ab6-8a466a8b4d39",
"ValidationErrors": []
}
],
"SubTotal": 365.82,
"TotalTax": 30.18,
"Total": 396.0,
"UpdatedDateUTC": "/Date(1229650679057+0000)/",
"CurrencyCode": "USD"
}
Updating Invoices
The way you make the request to update an invoice is nearly similar to the way you fetch them, except that you’ll need to prepare the invoice with the data that you want to update, then use the POST method to update them in Xero.
Here is how you can do that in python. In this case, I want to update the InvoiceNumber
of the invoice to My InvoiceNumber
, and this is how I do that with python:
import requests # import the requests module, required to make API calls
from copy import deepcopy # import this function to make a copy from the original invoice
invoice_url = "<https: 2.0="" api.xero.com="" api.xro="" invoices="">" # url to fetch
access_token = "<your access="" token="">" # your Access Token returned from the Token API call
invoice_id = "YOUR INVOICE ID" # the ID of the invoice that you want to fetch
# url to fetch the invoice
url = f"{invoice_url}{invoice_id}"
# paste here your invoice content returned from the previous step
invoice = "<your as="" content="" dict="" invoice="">"
# make a copy from the original invoice
update_invoice = deepcopy(invoice)
# change the InvoiceNumber to the value you want, in here is My InvoiceNumber
update_invoice["InvoiceNumber"] = "My InvoiceNumber"
# prepare the headers for the API call
headers = {
"Authorization": f"Bearer {access_token}", # required for authentication
"Accept": "application/json", # specify the type of data you expected to accept from response
}
# make the request
resp = requests.post(url, headers=headers, json=update_invoice)
# check the response status code, if it is 200 then it was successful
if resp.status_code == 200:
resp_data = resp.json()
# we need to check again the status of the response
# sometimes, the Xero API return 200 for the status code but
# the content of the response is an error message
if resp_data["Status"] != "OK":
raise Exception(f"Failed to request invoice. {resp_data}")
invoices = resp_data["Invoices"]
# because the API returns a list of invoices, eventhough we only use an
# invoice ID to fetch, so that we need to print out the first item in the list
print(invoices[0])</your></your></https:>
After the API call is completed, then now you can see that the invoice is updated with the InvoiceNumber
as My InvoiceNumber
{
"Type": "ACCREC",
"InvoiceID": "fee88eea-f2aa-4a71-a372-33d6d83d3c45",
"InvoiceNumber": "My InvoiceNumber",
"Reference": "Ref MK815",
"Prepayments": [],
"Overpayments": [],
"AmountDue": 396.0,
"AmountPaid": 0.0,
"SentToContact": False,
"CurrencyRate": 1.0,
"IsDiscounted": False,
"HasErrors": False,
"InvoicePaymentServices": [],
"Contact": {
"ContactID": "5b96e86b-418e-48e8-8949-308c14aec278",
"ContactStatus": "ACTIVE",
"Name": "Marine Systems",
"EmailAddress": "",
"BankAccountDetails": "",
"Addresses": [
{
"AddressType": "POBOX",
"City": "",
"Region": "",
"PostalCode": "",
"Country": "",
},
{
"AddressType": "STREET",
"City": "",
"Region": "",
"PostalCode": "",
"Country": "",
},
],
"Phones": [
{
"PhoneType": "FAX",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": "",
},
{
"PhoneType": "MOBILE",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": "",
},
{
"PhoneType": "DDI",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": "",
},
{
"PhoneType": "DEFAULT",
"PhoneNumber": "",
"PhoneAreaCode": "",
"PhoneCountryCode": "",
},
],
"UpdatedDateUTC": "/Date(1721554028797+0000)/",
"ContactGroups": [],
"IsSupplier": False,
"IsCustomer": True,
"DefaultCurrency": "USD",
"SalesTrackingCategories": [],
"PurchasesTrackingCategories": [],
"ContactPersons": [],
"HasValidationErrors": False,
},
"DateString": "2024-07-15T00:00:00",
"Date": "/Date(1721001600000+0000)/",
"DueDateString": "2024-07-21T00:00:00",
"DueDate": "/Date(1721520000000+0000)/",
"BrandingThemeID": "d613f7f9-8fcb-477f-97f0-31eb85b7e5cf",
"Status": "AUTHORISED",
"LineAmountTypes": "Inclusive",
"LineItems": [
{
"Description": "Marketing guides",
"UnitAmount": 99.0,
"TaxType": "OUTPUT",
"TaxAmount": 30.18,
"LineAmount": 396.0,
"AccountCode": "200",
"Tracking": [],
"Quantity": 4.0,
"LineItemID": "8dd05881-be4a-4765-b95e-cce0d7395f9f",
"AccountID": "d1ebb97b-d207-4ccb-9ab6-8a466a8b4d39",
"ValidationErrors": [],
}
],
"SubTotal": 365.82,
"TotalTax": 30.18,
"Total": 396.0,
"UpdatedDateUTC": "/Date(1721554028950+0000)/",
"CurrencyCode": "USD",
}
Now that you know how to update an invoice using the Xero API, you can explore updating other fields within the invoice as well. The Xero API provides comprehensive functionality to manage various aspects of your invoices. For more detailed information on how to make these updates, please refer to the official Xero API documentation on invoices.
Conclusion
Congratulations! You now know how to make API calls to Xero using Python, which can greatly improve your efficiency in managing invoices. This knowledge can be extended to integrate other functionalities of Xero into your applications.