Setting Up the Xero Integration#
This guide walks through everything required to connect Cobalt to Xero — from creating the Xero app in the developer portal to verifying the connection. It covers both the test/demo and production environments.
Cobalt uses Xero’s Custom Connection (machine-to-machine OAuth 2.0 client credentials). There is no user-facing consent screen or callback URL. Cobalt exchanges its client credentials directly for an access token whenever it needs to make API calls.
For the developer API reference (XeroApi methods, models, testing patterns) see Using the Xero Integration.
Prerequisites#
A Cobalt environment is already running (test server or production).
You have a Xero account with admin access to the target organisation.
You know the Elastic Beanstalk (or local) environment variable configuration mechanism for your environment.
Xero Account Requirements#
Before setting up the integration, the Xero organisation (and your Xero account) must meet the following requirements.
Subscription plan#
Cobalt uses the Xero Accounting API (contacts, invoices, payments endpoints). This is available on all standard Xero paid plans. Custom Connection apps — which Cobalt uses for authentication — are available to any Xero account holder through the developer portal at no extra cost.
Note
Xero’s plan names and feature inclusions change over time. Verify that the target organisation’s plan includes API access on Xero’s pricing page before proceeding.
For testing, every Xero account includes a Xero demo company at no charge. All development and UAT work should be done against the demo company.
Developer portal access#
Creating a Custom Connection app requires access to the Xero Developer Portal. Any Xero account holder can log in to the developer portal and create apps for free — no separate developer subscription is needed.
Chart of accounts#
Cobalt creates invoices and records payments against specific GL account codes. Those accounts must exist in the Xero chart of accounts before Cobalt can use them. Two account codes are configured via environment variables:
Purpose |
Cobalt setting |
Required Xero account type |
Notes |
|---|---|---|---|
Payment clearing account |
|
Bank |
Used when |
Settlement payables account |
|
Current Liability |
Used on the ACCPAY settlement bill (no GST) and the informational |
Fee income account |
|
Revenue or Other Income |
Used on the fee-recovery line of the ACCREC fee invoice (with GST).
Must be a revenue-type account so that Xero accepts |
Additionally, create_invoice() accepts arbitrary account codes per line item.
These are passed by the calling code and not stored in environment variables —
create the appropriate Revenue or Other Income accounts in Xero for each income
category (affiliation fees, table fees, etc.).
Tax types#
Cobalt sends LineAmountTypes: "Inclusive" on all invoices, meaning every
UnitAmount passed to Xero already includes any applicable GST. Cobalt
uses two distinct tax types for the automatically-created settlement invoices:
Cobalt setting |
Default value |
Where used |
|---|---|---|
|
|
Line items on the ACCPAY settlement invoice (the bill paid to the
club) and the informational |
|
|
The fee-recovery line item on the ACCREC fee invoice (the invoice
billed to the club for the ABF’s processing fee). The ABF charges GST
on this service fee. |
Both values are Xero tax type codes. Valid codes for Australian organisations
include OUTPUT (GST on income), INPUT (GST on expenses),
BASEXCLUDED (BAS excluded), and NOTAX (no tax / exempt). Check
Accounting → Tax Rates in Xero for the exact codes available in your
organisation.
The create_invoice() method (used for manually-created invoices) also
accepts an optional tax_type per line item; if omitted, no TaxType
is sent and Xero uses the account’s default tax setting.
Step 1 — Create a Xero Custom Connection App#
A separate Xero app (Custom Connection) is required for each Cobalt environment because each environment connects to a different Xero organisation (demo company for non-production, production org for production).
Log in to the Xero Developer Portal.
Make sure you are connected to the Demo Company (AU)
Click New app.
Fill in the form:
Field
Value
App name
Cobalt(or any descriptive name)Integration type
Custom Connection
Company or application URL
https://www.myabf.com.au(or your hostname)Click Create app.
Note
Custom Connection apps do not use redirect URIs. There is no OAuth consent screen — authentication is handled entirely server-side using client credentials.
Configuring the Custom Connection#
After creating the app you must configure which Xero organisation it connects to and what API scopes it requires:
In the app settings, go to the Configuration tab.
Under Connected organisation, select the Xero organisation this app should access (demo company for test/UAT, production org for production).
Under Scopes, enable at minimum:
accounting.contactsaccounting.transactionsaccounting.settings.read
Save the configuration.
Copy the Client ID and Client Secret — you will need them in Step 2.
Warning
Because a Custom Connection app is bound to a single Xero organisation, you need a separate app for each environment that connects to a different Xero organisation. All environments that share the same demo company can share one app.
Step 2 — Set Environment Variables#
Set the following variables in the environment configuration for each Cobalt
environment (Elastic Beanstalk environment properties, local .env file, or
shell exports):
Variable |
Description |
|---|---|
|
Client ID from the Xero developer portal |
|
Client secret from the Xero developer portal |
|
Xero account code for the bank/clearing account used when recording
payments (e.g. |
|
Xero account code for the settlement payables account (Current Liability). Used on the ACCPAY bill and the informational lines of the ACCREC invoice. |
|
Xero account code for the fee income account (Revenue / Other Income).
Used on the fee-recovery line of the ACCREC invoice. Must be a revenue
account so that |
|
Xero tax type code for the settlement disbursement lines (no GST).
Default: |
|
Xero tax type code for the processing-fee recovery line on ACCREC
invoices. Default: |
Test vs production values#
Use a Xero demo company for all non-production environments. Because the Custom Connection app is bound to a single organisation in the Xero developer portal, using a demo company simply means creating a separate Custom Connection app that is linked to the demo company (see Step 1).
Warning
Never configure a non-production Custom Connection app to connect to the production Xero organisation. Test code will create real invoices and contacts in the live account.
Finding account codes#
In Xero: Accounting → Chart of Accounts. The code for each account is shown in the Code column. Common codes used by the ABF:
Variable |
Example code |
Typical account name |
|---|---|---|
|
|
Business bank account or clearing account |
|
|
Club settlement payable / accounts payable clearing |
The exact codes depend on the chart of accounts for your Xero organisation.
Step 3 — Connect Cobalt to Xero#
With the Custom Connection there is no user-facing OAuth consent screen. Cobalt fetches an access token directly using its client credentials. You trigger this once from the admin UI to populate the tenant ID and verify the credentials work.
Start (or deploy) the Cobalt application with the environment variables from Step 2 in place.
Log in to Cobalt as an ABF staff user (the
/xero/views are restricted to staff).Navigate to
/xero/.Click Connect. Cobalt will:
POST to
https://identity.xero.com/connect/tokenwith the client credentials to obtain a fresh access token.Decode the JWT access token to extract the
authentication_event_id.Call
GET https://api.xero.com/connections(withXero-User-Idset to theauthentication_event_id) to retrieve the linked tenant.Save the access token, its expiry time, and the tenant UUID to the
XeroCredentialsdatabase table.
Verify that the Configuration panel shows:
A non-empty tenant ID
A non-empty access token
An expiry time roughly 30 minutes in the future
If any of these are missing, check the application logs for errors from
xero.core and click Connect again.
Token lifecycle#
Access tokens expire after ~30 minutes. Cobalt fetches a new one automatically
before every API call via refresh_xero_tokens() — no manual intervention is
required. Because client credentials are used, there is no refresh token; a
brand-new access token is obtained directly whenever the current one has expired.
As long as XERO_CLIENT_ID and XERO_CLIENT_SECRET remain valid,
the integration will keep working indefinitely without any re-authorisation step.
Step 4 — Verify the Connection#
Use the API playground on the Xero admin home page (/xero/) to confirm
everything is working:
Select List contacts from the command dropdown and click Run.
The response panel should show a list of contacts from the connected Xero organisation. If the demo company is new it may be empty — that is fine.
If you receive an error, check:
The environment variables were loaded correctly (restart the app server after changing them).
The Custom Connection app in the Xero developer portal is configured for the correct Xero organisation and has the required scopes enabled.
Step 5 — Set Up GL Account Codes in Xero#
Before invoices can be created, the Xero chart of accounts must contain the
account codes referenced in XERO_BANK_ACCOUNT_CODE and
XERO_PAYABLE_ACCOUNT_CODE, and any line-item account codes passed to
create_invoice(). Each account must be the correct Xero account type —
using the wrong type will cause API errors.
In the demo company, Xero pre-populates a standard chart of accounts. You may need to add or modify accounts to match the codes used in production.
To add or edit an account: Accounting → Chart of Accounts → Add Account (or click an existing account to edit it).
Payment clearing account (XERO_BANK_ACCOUNT_CODE)#
This account is referenced by create_payment() when recording that an
invoice has been paid.
Xero account type:
BankEnable payments: must be enabled (Xero does this automatically for Bank accounts)
Tax: not applicable to Bank accounts
Bank-type accounts appear under the Banking menu in Xero and can be linked to a bank feed for reconciliation. The ABF typically maps this to the actual business transaction account (e.g. code
090).
Note
Only Bank-type accounts can be used as the payment account in Xero’s Payments API. Attempting to record a payment against a non-Bank account will return an error.
Settlement payables account (XERO_PAYABLE_ACCOUNT_CODE)#
Used on the ACCPAY settlement invoice (the bill paid to the club) and on the
informational $0 lines of the ACCREC fee invoice.
Xero account type:
Current Liability(orAccounts Payable)Tax type:
BASEXCLUDED/NOTAX— no GST applies. The ABF is disbursing money that was always the club’s; it is not the ABF’s income.When the ABF transfers money to a club and reconciles the bank transaction in Xero, the ACCPAY invoice is marked paid and this liability is cleared.
Fee income account (XERO_FEE_ACCOUNT_CODE)#
Used on the fee-recovery line item of the ACCREC fee invoice (the charge billed to the club for the ABF’s processing fee).
Xero account type:
RevenueorOther IncomeTax type:
OUTPUT(10% GST on income) — the ABF charges GST on its processing fee because it is taxable income for the ABF.This must be a revenue-type account. Current Liability accounts (such as
XERO_PAYABLE_ACCOUNT_CODE) cannot acceptOUTPUTtax type in Xero, which would cause invoice creation to fail with the error “The TaxType code ‘OUTPUT’ cannot be used with account code ‘NNN’.”
Revenue / income accounts (create_invoice() line items)#
The generic create_invoice() method (used for manually-created invoices such
as affiliation fees) references revenue account codes passed by the calling code.
These codes are not stored in Cobalt’s environment variables.
Xero account type:
RevenueorOther IncomeTax: Cobalt now sends
LineAmountTypes: "Inclusive"on all invoices. If a line item does not include an explicittax_type, Xero uses the account’s default tax setting. Set the account default to match what is expected (e.g.OUTPUTfor taxable income,BASEXCLUDEDfor non-taxable).The Create Invoice tool in the Xero admin screen (
/xero/) includes a Tax Type dropdown so the correct rate can be selected per line item.Create a separate account for each income category, e.g.:
Affiliation fees
Table fees
Entry fees
Ensure the account code strings match exactly what is passed by the Cobalt code
that calls create_invoice() — they are case-sensitive in Xero.
Verifying account setup#
Confirm the accounts are configured correctly by creating a test invoice and payment via the Django shell:
from xero.core import XeroApi
from organisations.models import Organisation
xero = XeroApi()
org = Organisation.objects.get(pk=<id>) # must have xero_contact_id set
# Test ACCREC invoice with a revenue account code
invoice = xero.create_invoice(
organisation=org,
line_items=[{"description": "Test", "quantity": 1, "unit_amount": 1.00, "account_code": "200"}],
reference="TEST",
)
print(invoice)
# Test recording a payment (uses XERO_BANK_ACCOUNT_CODE)
if invoice:
xero.create_payment(invoice.xero_invoice_id, amount=1.00)
print("Payment recorded")
Onboarding an Organisation#
Before invoices can be raised for a club or organisation, a corresponding Xero
Contact must exist and its UUID must be saved in
Organisation.xero_contact_id.
Via the admin UI#
Go to
/xero/→ API playground.Select Create contact, enter the organisation ID, and click Run.
On success the
Organisation.xero_contact_idfield is populated automatically.
Via code (e.g. Django shell)#
from xero.core import XeroApi
from organisations.models import Organisation
xero = XeroApi()
org = Organisation.objects.get(pk=<id>)
contact_id = xero.create_organisation_contact(org)
print(f"Contact ID: {contact_id}")
Step 6 — Configure Xero Webhooks#
Xero can push real-time notifications to Cobalt whenever an invoice is created
or updated. This is the primary mechanism Cobalt uses to keep XeroInvoice
status fields in sync — webhooks eliminate the need to poll Xero for every
outstanding invoice.
Setting up the webhook in the Xero developer portal#
Log in to the Xero Developer Portal and open your Custom Connection app.
Go to the Webhooks tab.
Click Add Webhook and fill in:
Field
Value
Webhook URL
https://<your-hostname>/xero/webhook(e.g.https://www.myabf.com.au/xero/webhook)Event types
Select Invoices (covers both CREATE and UPDATE events)
Click Save. Xero will immediately send an Intent to Receive validation request — a POST with an empty
eventslist and a valid HMAC-SHA256 signature. Cobalt verifies the signature and returns HTTP 200, completing the handshake automatically.Copy the Webhook key shown on the Webhooks tab. This is the signing secret used to verify every incoming request.
Setting XERO_WEBHOOK_KEY#
Add the webhook signing key as an environment variable:
Variable |
Description |
|---|---|
|
The webhook signing key from the Xero developer portal Webhooks tab. |
How the webhook endpoint works#
Every request Xero sends carries an x-xero-signature header containing a
Base64-encoded HMAC-SHA256 signature of the raw request body, signed with the
webhook key. Cobalt:
Recomputes the HMAC-SHA256 using
XERO_WEBHOOK_KEY.Compares it to the header using a constant-time comparison. If they do not match, returns HTTP 401 (and logs a warning).
For each event with
eventCategory == "INVOICE", fetches the current invoice status from Xero and updates the localXeroInvoicerecord if the status has changed.Returns HTTP 200.
The endpoint is exempt from CSRF checks (it is machine-to-machine) and is allowed through maintenance mode so Xero can always reach it.
Note
The Intent to Receive handshake sends a payload with "events": [].
Cobalt handles this correctly — it verifies the signature and returns 200
without attempting to process any events.
Cron Job — Invoice Status Sync (fallback)#
Cobalt also includes a management command that polls Xero for outstanding invoice statuses. Now that webhooks provide real-time updates, this command serves as a daily fallback to catch any events that may have been missed (e.g. during downtime):
python manage.py sync_xero_invoice_status
Schedule this as a daily cron job on the EC2 instances. Running it hourly is
no longer necessary once webhooks are configured. The command is safe to run
on all environments; it only reads from Xero and updates the local
XeroInvoice.status field.
Troubleshooting#
Symptom |
Fix |
|---|---|
|
The client credentials are invalid or the Custom Connection app has been
disabled/revoked in the Xero developer portal. Check |
Token fetch returns an error instead of |
The client credentials are wrong, or the app’s scopes have not been saved correctly in the Xero developer portal. Re-check the app configuration. |
Account code not found error on invoice creation |
The account code passed to |
Fee invoice shows |
The auto-payment step after uploading the fee invoice failed. Check the
|
GST not appearing on the fee recovery invoice |
|
Token refresh succeeds but tenant ID is blank |
|
Summary of Environment Variables#
Variable |
Required |
Notes |
|---|---|---|
|
Yes |
From Xero developer portal. Each environment that connects to a different Xero organisation needs its own Custom Connection app and client ID. |
|
Yes |
From Xero developer portal. |
|
Yes |
GL account code for the bank/clearing account used when recording payments. Must be a Bank-type account in Xero. |
|
Yes |
GL account code for the settlement payables account (Current Liability). Used on ACCPAY bills and the informational lines of ACCREC fee invoices. |
|
Yes |
GL account code for the fee income account (Revenue / Other Income).
Used on the fee-recovery line of the ACCREC invoice. Must accept
|
|
No |
Xero tax type for settlement disbursement lines. Default: |
|
No |
Xero tax type for the fee-recovery line on ACCREC invoices.
Default: |
|
Yes (if webhooks are configured) |
Signing key from the Xero developer portal Webhooks tab. Required to verify the HMAC-SHA256 signature on incoming webhook requests. |
Resetting the Demo Company#
The demo company will reset back to the defaults after about 28 days. If you want to reset it now, you can login and visit: https://my.xero.com/!xkcD/Dashboard At the bottom in very small print is the link to reset it.
Resetting the Demo Company breaks the Custom Connection so this needs to be deleted and rebuilt.
After rebuilding it, you will probably want to create the clubs in Xero. To do this, ssh to the test server and run:
./manage.py shell_plus
>>> Organisation.objects.update(xero_contact_id="")
Exit shell_plus and run:
./manage.py create_missing_xero_contacts