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.