Automated Expensify Reimbursement Sync to Xero (n8n)

Seamlessly convert approved Expensify reports into Xero Bills with automated data mapping and receipt attachments using n8n.

Tools: ExpensifyXero

Platform: n8n

Short Answer

A self-hosted or cloud-based n8n workflow that triggers on report approval, maps Expensify categories to Xero Chart of Accounts codes automatically, and creates a pre-populated 'Awaiting Payment' Bill in Xero with the original receipt attached.

The Problem

Manual entry of expense reports into accounting software is prone to human error and delays month-end closing. Finance teams often struggle with mismatched account codes and missing receipt attachments between Expensify and Xero.

The Outcome

A self-hosted or cloud-based n8n workflow that triggers on report approval, maps Expensify categories to Xero Chart of Accounts codes automatically, and creates a pre-populated 'Awaiting Payment' Bill in Xero with the original receipt attached.

Step-by-Step Guide

1. **Credential Setup**: In n8n, go to Credentials > Add Credential. Create an 'Expensify API' credential (using Partner User ID/Secret) and a 'Xero OAuth2' credential. 2. **Expensify Trigger**: Add the 'Expensify' node. Set the Resource to 'Report' and Event to 'Status Change'. Use an Expression to filter only for reports where `{{ $json.status }} === 'Approved'`. 3. **Fetch Line Items**: Expensify's trigger often sends summary data. Use a second Expensify node with the 'Get' action to retrieve the full list of expense line items for the specific Report ID. 4. **Handle Binary Data**: To sync receipts, add an 'HTTP Request' node. Set the URL to the `receipt_url` from Expensify. Set 'Response Format' to 'File' to grab the actual image/PDF. 5. **Data Transformation (Code Node)**: Use a 'Code' node to transform the Expensify data. Create a JavaScript object that maps Expensify 'Tags' to Xero 'AccountCodes' (e.g., `if (tag === 'Travel') { accountCode = '430'; }`). 6. **Contact Matching**: Add a 'Xero' node. Use the 'Contact' resource and 'Get All' operation with a filter for the employee's email to find their Xero Contact ID. Add a 'Filter' node to create the contact if it doesn't exist. 7. **Create Xero Bill**: Add a 'Xero' node. Select 'Purchase Invoice'. Map the `total` to 'Amount', and use the output of your Code node for the 'Account Code'. Set the status to 'DRAFT' or 'SUBMITTED' for review. 8. **Attach Receipt**: Add another Xero node using the 'File' resource. Use the 'Upload' operation, referencing the 'Purchase Invoice ID' from the previous step and the binary file from step 4. 9. **Error Handling**: Create a separate 'Error Trigger' workflow. Use a 'Post to Slack' node to notify the finance team if a Xero Account Code mapping fails or if an attachment is too large.

Data Mapping

| Expensify Field | Xero Destination Field | n8n Expression / Transformation | | :--- | :--- | :--- | | `reportID` | Reference | `{{ $json.reportID }}` | | `total` | Line Amount | `{{ $json.total / 100 }}` (Expensify uses cents) | | `tag` | AccountCode | `{{ $node["Code Node"].json.accountCode }}` | | `employeeEmail` | Contact (Email) | `{{ $json.email }}` | | `receipt_url` | Attachment | Binary property from HTTP Request node | | `reportName` | Description | `{{ $json.reportName }}` | | `currency` | CurrencyCode | `{{ $json.currency }}` |

Gotchas & Failure Modes

* **Cents vs. Dollars**: Expensify often provides amounts in cents (integers). Use `{{ $json.amount / 100 }}` in your n8n expression to avoid overpaying employees by 100x. * **Rate Limiting**: Xero has a limit of 60 requests per minute. Use the n8n 'Wait' node or 'Split In Batches' node if processing more than 50 expense reports at once. * **Binary Data Persistence**: By default, binary data (receipts) might not persist through all nodes. Use the 'Wait' or 'Merge' node to ensure the file is available when the Xero Attachment node triggers. * **OAuth Refresh**: Ensure your n8n instance is accessible via a public URL (or use a tunnel) for Xero's OAuth2 callback to remain active; otherwise, credentials will expire every 30 minutes.

Verification Checklist

- [ ] **Test Trigger**: Manually 'Approve' a report in Expensify and check n8n 'Execution' tab for the webhook arrival. - [ ] **Account Code Mapping**: Verify the Code Node correctly assigns Xero ID '400' when Expensify Tag is 'Meals'. - [ ] **Binary Check**: In the n8n UI, confirm the HTTP Request node shows a thumbnail of the receipt in the 'Binary' tab. - [ ] **Xero Mock**: Run the workflow and check Xero 'Business > Bills to Pay > Draft' to see if the invoice appears with the correct total. - [ ] **Attachment Audit**: Click into the Xero Bill and ensure the 'File' icon shows the uploaded Expensify receipt.

Ready to Automate?

Build this automation with n8n in minutes.