Skip to main content
RapidDev - Software Development Agency
flutterflow-integrationsFlutterFlow API Call

Xero

Connect FlutterFlow to Xero using a FlutterFlow API Call pointed at a Firebase or Supabase proxy that handles OAuth 2.0 and injects the mandatory Xero-tenant-id header on every request. The essential first step after OAuth: call Xero's /connections endpoint to retrieve the tenant ID for the connected organisation — without it, every API request fails even with a valid token, and the error looks identical to a bad credential.

What you'll learn

  • How to create a Xero developer app and configure OAuth 2.0 scopes for accounting data access
  • How to deploy a Firebase or Supabase proxy that runs the OAuth Authorization Code flow and stores the access token and tenant ID
  • Why you must call Xero's /connections endpoint after OAuth to get the Xero-tenant-id, and how to store it per organisation
  • How to configure a FlutterFlow API Group to call your proxy for invoices, bills, and bank transactions
  • How to cache Xero responses in the proxy to respect per-tenant rate limits and speed up the app
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Advanced18 min read2-4 hoursFinance & AccountingLast updated July 2026RapidDev Engineering Team
TL;DR

Connect FlutterFlow to Xero using a FlutterFlow API Call pointed at a Firebase or Supabase proxy that handles OAuth 2.0 and injects the mandatory Xero-tenant-id header on every request. The essential first step after OAuth: call Xero's /connections endpoint to retrieve the tenant ID for the connected organisation — without it, every API request fails even with a valid token, and the error looks identical to a bad credential.

Quick facts about this guide
FactValue
ToolXero
CategoryFinance & Accounting
MethodFlutterFlow API Call
DifficultyAdvanced
Time required2-4 hours
Last updatedJuly 2026

The One Detail That Breaks Every Xero Integration: The Tenant ID Header

Xero's accounting API is well-documented and official, but it has one requirement that trips almost every first-time integrator: every single API request must include a Xero-tenant-id header specifying which Xero organisation you are querying. You cannot skip this header — even with a perfectly valid OAuth access token, requests without it return an error that looks identical to an authentication failure. The tenant ID is not obvious because Xero does not include it in the OAuth callback. You get it by making an additional API call to https://api.xero.com/connections immediately after the OAuth handshake. That /connections call returns a list of all organisations the user has authorised, each with a tenantId field. Your proxy extracts this ID, stores it alongside the OAuth tokens in Firestore or Supabase, and injects it as the Xero-tenant-id header on every subsequent accounting API request.

Xero is the dominant accounting platform in the UK, Australia, and New Zealand, which means it is often the right choice for apps serving customers in those markets. The FlutterFlow use case is a mobile invoices and bills dashboard — a business owner or accountant opening a phone app to check outstanding invoices and approve payments without logging into a desktop browser. The API gives you access to invoices, bills, bank transactions, contacts, accounts, and reports.

The security model is the same as with any finance API in FlutterFlow: the OAuth client secret and all tokens must never appear in your Flutter app bundle. FlutterFlow compiles to a Flutter app that runs on a user's device, and anything compiled into the binary can be extracted from a decompiled APK or IPA. The proxy is the only place credentials and tokens live. The FlutterFlow app only ever calls your proxy URL. Additionally, Xero enforces per-tenant rate limits of approximately 60 calls per minute and a daily limit per tenant — the proxy should cache invoice and contact lists for a few minutes rather than re-fetching on every screen navigation.

Integration method

FlutterFlow API Call

FlutterFlow API Calls are configured to hit a Firebase Cloud Function or Supabase Edge Function proxy that you deploy. The proxy runs the OAuth 2.0 Authorization Code flow, fetches the organisation's Xero-tenant-id from Xero's /connections endpoint, and includes it as a header on every subsequent API request. The FlutterFlow app never holds the client secret, OAuth tokens, or tenant ID — it only calls your proxy.

Prerequisites

  • A Xero developer account at developer.xero.com — free to create; a Xero subscription is required for live organisation data
  • A Xero app created in the developer portal with OAuth 2.0 credentials (client ID and client secret)
  • A Firebase project with Cloud Functions on the Blaze plan OR a Supabase project with Edge Functions
  • A FlutterFlow project on a paid plan (Starter or above for API Calls)
  • Basic familiarity with OAuth 2.0 Authorization Code flow and deploying serverless functions

Step-by-step guide

1

Step 1: Create a Xero developer app and configure OAuth scopes

Go to developer.xero.com and sign in with your Xero account. Navigate to My Apps and click New App. Choose Web app as the integration type (this enables the Authorization Code flow required for user-authorised data access). Give your app a name and enter your redirect URI — this must be the callback URL of your Firebase or Supabase proxy function. You will update this after deploying the proxy, but register a placeholder now. Once the app is created, open it and find the Client ID and Client Secret on the Configuration tab. These are the two credentials your proxy will use. Store them — you will add them to Firebase Functions config in the next step. The Client Secret is shown only once in some Xero plans; copy it immediately to a safe location. For scopes, you need at minimum: openid profile email offline_access accounting.transactions accounting.contacts accounting.settings. The offline_access scope is required to receive a refresh token — without it, the OAuth response will not include a refresh_token and the connection will expire after each access token's lifetime (~30 minutes). The accounting.transactions scope covers invoices, bills, and bank transactions. The accounting.contacts scope covers customers and vendors. Add additional scopes (accounting.reports.read, accounting.journals.read) only if your app needs them — minimal scopes are best practice.

Pro tip: Xero offers a Demo Company you can connect to your developer app without a paid subscription. This is perfect for testing your integration end-to-end before connecting a real organisation.

Expected result: Your Xero app is configured with a client ID, client secret, redirect URI, and the offline_access and accounting.* scopes. The Demo Company is available for test connections.

2

Step 2: Deploy a proxy that runs the OAuth flow and fetches the tenant ID

Your Firebase Cloud Function proxy needs to handle three stages: (1) redirect the user to Xero's OAuth authorization URL, (2) receive the callback, exchange the code for tokens, then immediately call /connections to get the tenant ID, and (3) on every subsequent request, use the stored access token plus tenant ID to call the Xero API. The OAuth authorization URL is: https://login.xero.com/identity/connect/authorize with parameters: response_type=code, client_id, redirect_uri, scope (URL-encoded), and state (a CSRF nonce you generate and verify on callback). In the callback function, exchange the code for tokens by POSTing to https://identity.xero.com/connect/token with Basic auth (base64 of client_id:client_secret) and the code in an application/x-www-form-urlencoded body. Immediately after receiving the access_token, make a GET request to https://api.xero.com/connections with the access token as a Bearer header. The response is a JSON array of connected organisations; each has a tenantId and tenantName field. Extract the tenantId and store it in Firestore alongside the access_token and refresh_token. This stored tenantId becomes the Xero-tenant-id header on every future accounting API request. For token refresh, POST to https://identity.xero.com/connect/token with grant_type=refresh_token. Xero's refresh tokens do not rotate on every use (unlike QuickBooks), but they can expire — refresh when the access token returns a 401 and write the new access token to Firestore.

index.js
1// Firebase Cloud Function proxy — index.js
2const functions = require('firebase-functions');
3const admin = require('firebase-admin');
4const axios = require('axios');
5admin.initializeApp();
6
7const XERO_TOKEN_URL = 'https://identity.xero.com/connect/token';
8const XERO_API_BASE = 'https://api.xero.com/api.xro/2.0';
9const XERO_CONNECTIONS_URL = 'https://api.xero.com/connections';
10
11// OAuth callback — exchanges code for tokens, fetches tenant ID
12exports.xeroCallback = functions.https.onRequest(async (req, res) => {
13 const { code } = req.query;
14 const cfg = functions.config().xero;
15 const basicAuth = Buffer.from(`${cfg.client_id}:${cfg.client_secret}`).toString('base64');
16
17 // Exchange code for tokens
18 const tokenResp = await axios.post(XERO_TOKEN_URL,
19 `grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(cfg.redirect_uri)}`,
20 { headers: { Authorization: `Basic ${basicAuth}`, 'Content-Type': 'application/x-www-form-urlencoded' } }
21 );
22 const { access_token, refresh_token } = tokenResp.data;
23
24 // CRITICAL: fetch the tenant ID immediately after receiving the access token
25 const connResp = await axios.get(XERO_CONNECTIONS_URL,
26 { headers: { Authorization: `Bearer ${access_token}` } }
27 );
28 const tenantId = connResp.data[0].tenantId; // first connected org
29 const tenantName = connResp.data[0].tenantName;
30
31 // Store tokens + tenant ID together — all three are required for every API call
32 await admin.firestore().doc('xero/session').set({
33 access_token, refresh_token, tenant_id: tenantId, tenant_name: tenantName,
34 connected: true, updated: Date.now(),
35 });
36 res.send('<h1>Xero connected! You can close this window.</h1>');
37});
38
39// Proxy function: fetch invoices with Xero-tenant-id header
40exports.xeroGetInvoices = functions.https.onCall(async (data, context) => {
41 const doc = await admin.firestore().doc('xero/session').get();
42 const { access_token, tenant_id } = doc.data();
43 const headers = {
44 Authorization: `Bearer ${access_token}`,
45 'Xero-tenant-id': tenant_id, // REQUIRED on every request
46 Accept: 'application/json',
47 };
48 try {
49 const resp = await axios.get(`${XERO_API_BASE}/Invoices?Statuses=AUTHORISED,DRAFT`, { headers });
50 return { invoices: resp.data.Invoices || [] };
51 } catch (err) {
52 if (err.response?.status === 401) {
53 // Refresh token and retry
54 const basicAuth = Buffer.from(`${functions.config().xero.client_id}:${functions.config().xero.client_secret}`).toString('base64');
55 const { refresh_token } = doc.data();
56 const refreshResp = await axios.post(XERO_TOKEN_URL,
57 `grant_type=refresh_token&refresh_token=${encodeURIComponent(refresh_token)}`,
58 { headers: { Authorization: `Basic ${basicAuth}`, 'Content-Type': 'application/x-www-form-urlencoded' } }
59 );
60 const newToken = refreshResp.data.access_token;
61 await admin.firestore().doc('xero/session').update({ access_token: newToken, updated: Date.now() });
62 headers.Authorization = `Bearer ${newToken}`;
63 const retry = await axios.get(`${XERO_API_BASE}/Invoices?Statuses=AUTHORISED,DRAFT`, { headers });
64 return { invoices: retry.data.Invoices || [] };
65 }
66 throw err;
67 }
68});

Pro tip: If your app will support multiple connected Xero organisations, store sessions keyed by user ID (qb_sessions/{userId}) rather than a single 'xero/session' document. Pass a userId from FlutterFlow so the proxy fetches the correct tenant ID.

Expected result: The xeroCallback function stores the access_token, refresh_token, and tenant_id in Firestore after a successful OAuth flow. The xeroGetInvoices function correctly includes the Xero-tenant-id header and returns invoices.

3

Step 3: Store the tenant ID and configure per-user or per-org sessions

The /connections step in your OAuth callback returns a JSON array where each element represents one Xero organisation the user has connected. For most use cases, you work with one organisation. Extract the first element's tenantId and tenantName. If your app might have users connected to multiple Xero organisations (e.g., a bookkeeper managing several clients), store the full list of {tenantId, tenantName} pairs in Firestore and add a way for the user to select the active organisation in FlutterFlow. In FlutterFlow, add an App State variable called activeXeroOrg (String) that holds the selected tenantId. In the proxy, accept the tenantId as a request parameter rather than always reading it from Firestore — this lets the FlutterFlow app switch between organisations by passing a different tenantId. For a single-org app, hardcode the lookup to the Firestore session document. After storing the tenant ID, trigger a Firestore document write (connected: true) that your FlutterFlow app listens to. Use a FlutterFlow Firebase Realtime Listener on the xero/session document's connected field. When it flips to true, navigate the user from a 'Connecting...' screen to the main dashboard. This pattern handles the round-trip: the user opens a WebView or browser for the OAuth flow, completes it on the Xero web interface, and FlutterFlow detects the completion via the Firestore listener without the user having to tap anything.

Pro tip: When the user has multiple Xero organisations, show them a small selection list (populated from the /connections array stored in Firestore) immediately after the OAuth flow completes. Let them choose which organisation to connect before navigating to the dashboard.

Expected result: The Firestore xero/session document contains access_token, refresh_token, and tenant_id after the OAuth flow. The FlutterFlow app detects the connection via a Realtime Listener and navigates to the dashboard.

4

Step 4: Configure a FlutterFlow API Group pointing at your proxy

Open your FlutterFlow project and click API Calls in the left navigation panel. Click + Add, then Create API Group. Name it XeroProxy. Set the Base URL to your Firebase Functions URL: https://us-central1-YOUR-PROJECT-ID.cloudfunctions.net. Add the first API Call: click + Add API Call, name it GetInvoices. Set method to POST (Firebase callable functions require POST). Set the path to /xeroGetInvoices. In the Body tab, set a JSON body of {} — the proxy handles authentication and tenant ID injection internally, so no parameters are needed in the body for basic calls. Add optional variables (like statusFilter or dateFrom) in the Variables tab if you want the FlutterFlow app to filter results. In the Response & Test tab, paste a sample invoice response from Xero (see the code block below). Click Generate JSON Paths. You will see paths like $.invoices[0].InvoiceID, $.invoices[0].AmountDue, $.invoices[0].Contact.Name, $.invoices[0].DueDate. Create a Data Type called XeroInvoice with fields: invoiceId (String), contactName (String), total (Decimal), amountDue (Decimal), dueDate (String), status (String). Map the JSON paths to this Data Type. Add a second API Call called GetBankTransactions following the same pattern. Add more calls for Contacts, Accounts, or Reports as your app needs them. Each call is a POST to a separate proxy function that includes the Xero-tenant-id header and handles token refresh internally.

typescript
1// Paste as sample in FlutterFlow Response & Test for GetInvoices
2{
3 "invoices": [
4 {
5 "InvoiceID": "a9fc20e6-8b98-4783-b845-b4d4b6a3cbb0",
6 "InvoiceNumber": "INV-0042",
7 "Status": "AUTHORISED",
8 "Type": "ACCREC",
9 "Total": 1800.00,
10 "AmountDue": 1800.00,
11 "DueDate": "/Date(1738022400000+0000)/",
12 "Contact": {
13 "ContactID": "b52c1f35-7c43-4a57-8c7e-1e2b5f9a3291",
14 "Name": "Smithfield Ltd"
15 }
16 }
17 ]
18}

Pro tip: Xero returns dates as /Date(milliseconds)/ format (e.g., /Date(1738022400000+0000)/). In FlutterFlow, use a custom function or Data Type transformer to convert this to a human-readable date string before displaying it.

Expected result: The XeroProxy API Group is configured with a GetInvoices call. Clicking Test API Call returns a 200 with invoice data through your proxy. JSON paths are generated and mapped to the XeroInvoice Data Type.

5

Step 5: Build an invoices screen and bind Xero data to widgets

Add a new Screen named Invoices. Add a ListView widget and set its Backend Query to XeroProxy → GetInvoices. Set the list type to XeroInvoice. FlutterFlow calls the proxy on screen load and populates the list. Inside each ListView item, add a Container with card styling (border radius 12, subtle drop shadow). Inside the Container, place a Column: a Text widget for the contact name (bound to XeroInvoice.contactName), a Row with the invoice number (XeroInvoice.invoiceId) and due date (XeroInvoice.dueDate), and a Row showing the total amount and amount due. Apply conditional formatting on the amountDue text: if amountDue equals total, the invoice is fully outstanding — show it in red. If amountDue is 0, the invoice is paid — show it in green. If amountDue is between 0 and total, it is partially paid — show in amber. Above the ListView, add a summary Row with three Container cards: Total Outstanding (sum of amountDue from all invoices with amountDue > 0), Overdue (count of invoices with dueDate before today), and Paid This Month (count of invoices with status PAID and a payment date in the current month). Use FlutterFlow's List Filters and Aggregation functions to compute these without extra API calls. Add a pull-to-refresh gesture on the ListView. Wrap it in a RefreshIndicator and bind the refresh action to GetInvoices. To avoid hitting Xero's rate limits (approximately 60 calls per minute per tenant), add logic in the proxy to cache the invoices list in Firestore for 3 minutes — subsequent GetInvoices calls within that window return the cached data without calling Xero.

Pro tip: Xero returns amounts as raw numbers. Use FlutterFlow's currency formatting in the Text widget's binding panel to display them as £1,800.00 or $1,800.00 depending on the organisation's currency setting.

Expected result: The Invoices screen shows a scrollable list of Xero invoices with contact name, amounts, and due dates. Summary cards show totals at the top. Pull-to-refresh reloads data. Overdue invoices are visually highlighted.

6

Step 6: Cache responses to respect rate limits and handle 401 token expiry

Xero enforces per-tenant rate limits of approximately 60 API calls per minute and a daily call limit per tenant (check the current limits in Xero's developer documentation — verify current numbers as they may have changed). A mobile app that re-fetches invoices, contacts, and bank transactions on every screen navigation can easily exceed the per-minute limit during active use, resulting in 429 Too Many Requests errors. The solution is to cache Xero API responses in Firestore at the proxy level. When GetInvoices is called, the proxy first checks if a Firestore cache document exists and was written within the last 3 minutes. If so, it returns the cached data without calling Xero. If not, it calls Xero, writes the fresh data to Firestore cache, and returns it. This 3-minute cache window is a reasonable balance between freshness and rate-limit safety for a business that updates invoices every few hours. For 401 token expiry, the proxy's retry-with-refresh pattern from Step 2 handles the normal case transparently. However, if the Xero access token has been revoked (the user removed your app from their Xero My Apps settings), the refresh will also fail with an invalid_grant error. In this case, your proxy should return a JSON body with {"error": "authorization_revoked"} to FlutterFlow. Add a Conditional Action in FlutterFlow's GetInvoices error path: check for this error string and navigate to a Reconnect Xero screen that explains the connection was removed and prompts the user to reconnect. If you would prefer to hand the OAuth proxy setup, token management, and rate-limit caching to a team with FlutterFlow experience, RapidDev handles integrations like this every week — free scoping call at rapidevelopers.com/contact.

index.js
1// Add this caching block at the start of your xeroGetInvoices function
2const CACHE_TTL_MS = 3 * 60 * 1000; // 3 minutes
3const cacheDoc = await admin.firestore().doc('xero/cache/invoices/latest').get();
4if (cacheDoc.exists) {
5 const { data, cached_at } = cacheDoc.data();
6 if (Date.now() - cached_at < CACHE_TTL_MS) {
7 return { invoices: data, fromCache: true };
8 }
9}
10// ... call Xero API ...
11// After getting fresh data, write to cache:
12await admin.firestore().doc('xero/cache/invoices/latest').set({
13 data: freshInvoices,
14 cached_at: Date.now(),
15});

Pro tip: Make the cache TTL configurable via a Firestore config document so you can adjust it without redeploying. A 3-minute cache is fine for invoices; bank transactions might warrant a 1-minute cache for more freshness.

Expected result: The proxy serves cached invoice data within the 3-minute window, reducing Xero API calls. A 429 no longer occurs during normal app navigation. Authorization-revoked errors trigger a Reconnect screen in FlutterFlow.

Common use cases

Mobile invoices and bills dashboard for small business owners

A business owner opens a FlutterFlow app and sees their open invoices (money owed to them) and outstanding bills (money they owe) from Xero, without opening a laptop. They can filter by due date and tap to see line items. The app connects to Xero through a proxy and refreshes on demand via pull-to-refresh.

FlutterFlow Prompt

Show two tabs: Invoices (with customer name, amount, due date, and paid/unpaid status) and Bills (with vendor name, amount, and due date), all pulled from Xero.

Copy this prompt to try it in FlutterFlow

Bank reconciliation companion for an accountant

An accounting firm builds an app for their clients where bank transactions from Xero's Bank Transactions endpoint appear with their reconciliation status. Unreconciled transactions are highlighted, letting clients quickly flag items that need attention before the monthly bookkeeping session.

FlutterFlow Prompt

Display a list of bank transactions from Xero with the account name, amount, date, and reconciliation status. Highlight unreconciled transactions in a different colour.

Copy this prompt to try it in FlutterFlow

Financial summary widget for a multi-business dashboard

A founder managing two or three businesses uses a FlutterFlow app that shows a financial snapshot for each entity — total outstanding invoices, overdue invoices count, and current bank balance — fetched from each company's Xero organisation using the per-tenant connection stored in the proxy.

FlutterFlow Prompt

Show a card for each connected Xero organisation with total outstanding invoices, overdue invoice count, and current bank balance.

Copy this prompt to try it in FlutterFlow

Troubleshooting

Every API request returns an error despite a valid OAuth token — looks like an auth failure

Cause: The Xero-tenant-id header is missing from the request. This is the single most common Xero integration error. Xero requires this header on every accounting API call; without it, the request fails even with a perfectly valid token.

Solution: Confirm that your proxy function includes the Xero-tenant-id header on every call to the Xero Accounting API. Log the full outgoing request headers in your Cloud Function to verify the tenant_id is being read from Firestore and injected correctly. Double-check that the /connections call ran successfully and wrote the tenantId to Firestore before the session was marked as connected.

typescript
1// Every Xero API request MUST include this header
2const headers = {
3 Authorization: `Bearer ${access_token}`,
4 'Xero-tenant-id': tenant_id, // cannot be omitted
5 Accept: 'application/json',
6};

XMLHttpRequest error when testing from FlutterFlow web preview

Cause: The Firebase Cloud Function is missing CORS headers, so the browser blocks the request from FlutterFlow's preview origin.

Solution: Use Firebase callable functions (onCall) for all proxy endpoints — they handle CORS automatically. If you used onRequest for any function, add the cors npm package with origin: true to handle cross-origin requests from the FlutterFlow preview.

429 Too Many Requests from the Xero API during normal app use

Cause: The FlutterFlow app is making too many API calls per minute through the proxy, exceeding Xero's per-tenant rate limit of approximately 60 calls per minute.

Solution: Implement 3-minute Firestore caching in the proxy for all list endpoints (invoices, contacts, bank transactions). Only fetch fresh data when the cache is stale or the user explicitly pulls to refresh. If you need near-real-time data for specific endpoints, reduce the cache TTL for those, but ensure other endpoints have longer TTLs to stay within the overall limit.

The /connections call returns an empty array — tenant_id is never stored

Cause: The OAuth scopes did not include offline_access or the openid scope, or the user connected a Xero account they do not have admin access to. In some cases the /connections endpoint is called before the access token is fully propagated.

Solution: Verify that offline_access is included in your OAuth scopes. Check that the user has admin access to the Xero organisation they are connecting. Add a short retry (1-2 attempts with a 500ms delay) around the /connections call in case of transient propagation delays immediately after token exchange.

Best practices

  • Always include the Xero-tenant-id header on every accounting API request — this is the single requirement that breaks most Xero integrations when missed.
  • Never put the Xero client secret, access token, or tenant ID in FlutterFlow API Call headers, App Values, or Dart code — keep them in the proxy's server environment.
  • Call the /connections endpoint immediately after OAuth token exchange to get the tenant ID, and store it with the tokens in a single Firestore write.
  • Cache invoice and contact list responses in the proxy for 3 minutes to avoid hitting Xero's per-tenant rate limits during active app navigation.
  • Handle 401 token expiry in the proxy with a transparent refresh-and-retry pattern so users never see authentication errors during normal use.
  • Return a distinct error signal (e.g., authorization_revoked) to FlutterFlow when the Xero connection is fully revoked so the app can prompt reconnection rather than showing a cryptic error.
  • Build against Xero's Demo Company during development — it has realistic accounting data and does not require a paid Xero subscription.
  • Store sessions keyed by user ID if your app supports multiple users or multiple connected Xero organisations — never use a single shared session document for a multi-user app.

Alternatives

Frequently asked questions

Why do I need the Xero-tenant-id header on every request?

Xero's API is multi-tenancy aware — one OAuth token can be authorised against multiple Xero organisations, and the tenant ID tells Xero which organisation's data you are requesting. Without it, Xero cannot determine which organisation to query and returns an error. You must retrieve the tenant ID from the /connections endpoint immediately after the OAuth handshake and include it as a header on every subsequent accounting API request.

Can I put the Xero access token in a FlutterFlow variable so the app calls Xero directly?

No. The access token grants full accounting access to a business's invoices, bills, bank feeds, and contacts. Storing it in a FlutterFlow variable means it is held in device memory and potentially serialised to app storage. A decompiled APK or network interceptor on the device can expose it. The token must live exclusively in your Firebase Cloud Function or Supabase Edge Function.

What is the Xero rate limit and how do I stay within it?

Xero applies approximately 60 API calls per minute and a daily limit per tenant — verify the current exact numbers in Xero's developer documentation as they can change. To stay within these limits, cache list responses (invoices, contacts, bank transactions) in Firestore for 3 minutes in your proxy. Only make live Xero API calls when the cache is stale or the user explicitly requests a refresh.

Why is my Xero connection working fine but the invoice list is empty?

Two likely causes: the OAuth scope does not include accounting.transactions, so the token cannot read invoices; or the status filter in your query excludes the invoices you expect. Xero's Invoice endpoint defaults to AUTHORISED status — add Statuses=DRAFT,AUTHORISED,SUBMITTED to see more records. Also verify the tenant ID is for the correct connected organisation if the user has multiple Xero organisations.

How do I handle a user who removes my app from their Xero account?

When a user removes your app from Xero's My Apps settings, the access and refresh tokens are immediately revoked. The next proxy call will get a 401 that the refresh attempt also cannot fix, returning an invalid_grant error. Your proxy should catch this and return a clear error signal to FlutterFlow (e.g., authorization_revoked in the response body). The FlutterFlow app should then navigate to a Reconnect screen that guides the user through the OAuth flow again to re-establish the connection.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Integrations are where projects stall

We wire FlutterFlow integrations like this one every week — auth, webhooks, edge cases included. Fixed-price builds from $13K, delivered in 6–10 weeks.

Talk to an integration engineer

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.